TECH PLAY

Google Cloud

イベント

マガジン

技術ブログ

By Satoshi Imai ; Finatext Ltd. What I set out to support, how I built it on AWS, and the architectural philosophy that emerged along the way. Contracts are living things It’s tempting to think of a contract as something you sign once and file away. In practice, a contract is alive. It gets amended. Deadlines shift. New people join the project and need to get up to speed. Renewal windows open and close. Across the whole lifecycle, the people who need to act on a contract — in sales, in finance, in the legal team that supports them — all need to be looking at the same, current  picture. And that’s genuinely hard work. Not because anyone is doing anything wrong, but because it’s an inherent coordination challenge of the domain. The more an organization grows, the more contracts and stakeholders it has, and the more effort it takes to keep everyone aligned on the latest state of each one. This is true for any company and its partners alike — it’s just the nature of contract lifecycle management. So here’s a nice thought to build toward: what if the current state of every contract — its terms, its upcoming milestones, the status of what’s in flight — were something everyone involved could see, the same way, whenever they needed to? That kind of shared, always-fresh picture is exactly what lets both sides of a deal fulfill their contracts smoothly and with confidence. Less time spent chasing status, more time spent on the relationship. That’s what I built ContractOps to support. Not with a single clever prompt, but with a deliberate, down-to-earth architecture. This article is a tour of how I built it — on AWS, fully serverless — and the design philosophy I kept returning to at every turn. What ContractOps actually is Since this article is meant to be the canonical description of the system, let me draw the boundaries clearly before the tour begins. ContractOps is a multi-agent system that supports Legal Operations  — the contract and billing administration that a company’s sales and finance teams carry out, with the backing of the legal department. It is deliberately not a system that renders legal judgments or performs legal work on its own. The work the AI does is bounded and concrete: it structures, indexes, searches, and keeps watch over contract-related information, it assists with drafting (producing first drafts and edits for people to review), and it prepares the groundwork so a human can reach a decision faster. The final judgment always belongs to a human  — and, for anything carrying legal weight, to the legal team or a qualified professional. Human-in-the-loop isn’t a feature I bolted on at the end; as you’ll see, it’s wired into the data model itself. The system is a small pantheon of single-purpose agents, named after Greek deities — less for flair than because a good name makes a responsibility easy to remember. What matters here is that they fall into three layers: The people-facing layer. Themis is the Slack-native assistant people actually talk to — search, analysis, Q&A, and draft assistance — turning information that’s normally scattered across places into something you can simply ask about . Iris is the gateway in front of it, validating incoming Slack events and routing them by domain. The monitoring layer. A small fleet —  Argus , Chronos , and Cassandra  — that keeps the shared picture fresh, each from its own angle. (They’re the stars of Decision 7, so I’ll save the detail.) The knowledge layer. Behind both runs a data pipeline of specialist agents I call the Scribes  — the ones who turn raw documents into trustworthy, structured knowledge. You’ll meet them by name on the tour, exactly where their work comes up. One thing holds for every agent in the system: it reconciles facts, structures information, and notifies. None of them makes a legal call. Chronos can say “this milestone is coming up”; it never decides what should be done about it. That decision is a person’s. The substrate is fully serverless on AWS : AWS Lambda for compute, Amazon DynamoDB for state, Amazon Bedrock (Claude Sonnet 4.5) for inference, Amazon EventBridge for scheduling, Amazon S3 for intermediate artifacts — with Slack and Google Workspace integration at the edges. There is no external message queue. That omission turns out to be a feature, not a gap, and I’ll get to why. ContractOps system architecture (AWS + GCP) ContractOps at a glance: a fleet of single-purpose Lambda agents over a DynamoDB single source of truth, with model inference kept inside the AWS boundary. Now, the tour. Each stop is a design decision, framed the way I actually faced it: a problem anyone building serious LLM agents will hit, the call I made, and what it cost. A principle that kept coming back Before the specifics, one idea connects all of them, so let me name it. An LLM is a probabilistic reasoning engine . The physical data layer — file hierarchies, spreadsheet coordinates like A1:Z45, raw database schemas — is deterministic structure . When you let the two touch directly, the model ends up spending its reasoning on the wrong job: guessing where data lives and how to address it, instead of thinking about your actual question. The fix isn’t novel. It’s a return to principles software engineering settled decades ago — encapsulation, separation of concerns, the Information Expert principle (give a responsibility to the component that actually has the knowledge to fulfill it). Business logic was never asked to understand the physical layout of a database; the industry put a data-access layer in between. An LLM deserves the same courtesy. The further I went, the more this hardened into a full architectural paradigm, which I’ve since written up as a preprint —  [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . You don’t need the theory to follow this article; you need the decisions it produced. Here they are. Decision 1 — Don’t send the model exploring. Hand it a menu, not a map. The universal problem. The seductive shortcut in agent design is to grant the model broad access — “here’s read access to the database, go find what you need” — and trust its reasoning for the rest. Modern models are good enough to attempt this, which is exactly the trap. With no map, the agent resorts to trial and error: guessing table names, guessing column meanings, generating speculative queries. Tokens evaporate, the context window chokes on its own speculative garbage, and your system collapses into a spectacular, expensive hallucination loop. The approach. Revoke the model’s right to free exploration. Instead of accepting open-ended search against the physical layer, the tool presents a closed menu of logical options  — semantic IDs plus just enough metadata to choose between them. The model’s job collapses from open-ended generation (“write a query to find X”) into bounded selection (“pick from these”). Hallucination has nowhere to go, because every option on the menu provably exists. The trade-off. The moment you take away free exploration, you owe the model completeness . The menu must exhaustively cover what’s actually there — a candidate missing from the list is one the model can never know to ask for. Drilling down a hierarchy is fine (folders, then files within a chosen folder). Forcing the model to page sideways through “next page, next page” because one response was scoped too narrowly is not. Decision 2 — Pay the comprehension tax up front, not at runtime. The universal problem. Chatbot-era design is poisoned by the Zero-Latency Dogma — the absurd belief that a tool must answer in a second or two or it has failed. But genuinely understanding enterprise documents is expensive . A scanned contract needs OCR and layout reasoning. A richly formatted spreadsheet can encode meaning in cell colors and borders. Done properly, that takes real time — so, under pressure to respond instantly, tools skip the hard part and return whatever flat text they can grab in the moment. That’s how meaning gets dropped before the model ever sees it, with nothing to signal the loss. The approach. Move the heavy structural work off the runtime path entirely and do it asynchronously, ahead of time. In ContractOps, an upfront ETL pipeline absorbs the physical noise before the model is ever involved. Whether the source is a clean digital PDF or a skewed paper scan, the Scribes homogenize it into one rigorous semantic tree — articles, sections, items — expressed as structured Markdown. The model never spends a token interpolating a smudged character or guessing at a layout. The LLM works here too, but as the eyes of a deterministic ETL process , not as a live explorer. The payoff: by query time, the data is already known territory . The latency-versus-completeness dilemma that feels impossible at runtime simply dissolves, because the cost was paid in advance. The trade-off. It looks like you’re trading away instant freshness on brand-new data — but that’s an illusion. The “instant” alternative only ever handed the model impoverished flat text, so there was never any real freshness to lose. And data that genuinely is brand-new — dropped in mid-conversation — gets its own runtime path (Decision 5). The one honest cost is the engineering effort up front: the pipeline has to be designed and maintained. In return, the model can’t be caught off guard by structure that nobody mapped. On AWS. This is a textbook serverless ETL. EventBridge fires on a schedule → Clio (a lightweight Lambda) routes the work → Metis dispatches each document to Theia (vision OCR) or Eunomia (text structuring), both calling Bedrock → intermediate Markdown lands in S3 → structured results land in DynamoDB. Heavy, binary-dependent steps run in container-based Lambdas; everything else ships as a slim ZIP. Idle cost is essentially nothing, because nothing runs when nothing needs to. Clio data pipeline: AllocationMap → contract analysis The pre-runtime comprehension pipeline. Physical noise is absorbed asynchronously and turned into structured, queryable knowledge before any agent asks a question. Decision 3 — Split “finding” from “fetching” at the storage layer. The universal problem. Suppose your data is already beautifully structured. You drop it into one searchable store and let the agent query it. The first hit comes back — and drags a 40-page contract along with it, because the store doesn’t distinguish “the thing you searched for” from “the entire payload of that thing.” A couple of hits like that and the context window is gone. One careless search, total collapse. The approach. Separate the act of finding from the act of fetching , physically, into two stores with two jobs: A search index (the ContractOps table) holds only ultralight metadata — parties, execution dates, status, contract type. The agent scans this to grasp the landscape, with near-zero pressure on the context window. An extraction store (the ContractCodex table) holds the actual payloads — the structured clauses themselves. The flow becomes deterministic: the agent reviews the lightweight menu, picks a document ID and a clause key, and only then fetches exactly those clauses by ID. What used to be an uncertain similarity search across a giant text blob becomes a precise, two-step API call. Searching and reading are no longer the same operation, so reading can never accidentally flood the model. On AWS. Both stores live in DynamoDB under a single-table design, but their responsibilities are cleaved apart by purpose-built tables and secondary indexes — operational metadata indexed for the “find” path, contract knowledge partitioned for the “fetch” path. Same database technology; deliberately different access surfaces. Decision 4 — Don’t hide what you can’t fully trust. Make trust a column. The universal problem. AI-extracted data leaves you with two bad options. Trust it uncritically, and an OCR misread can propagate downstream unnoticed. Or gate every extracted clause behind human review before anyone can use it — honest, but it doesn’t scale. The approach. A third path: don’t block the data, grade it. A dedicated auditor agent, Dike , runs as the last step of the ETL pipeline — long before any model does semantic reasoning. It deterministically cross-checks the structured output against the raw text (using techniques like SHA-256 hashing), marks the indisputable matches as trusted, and marks anything with detected drift as requires verification . Crucially, it does not remove the uncertain data. Everything stays searchable; only its trustworthiness becomes explicit metadata  — a “trust boundary” baked right into the schema. This is what it means to treat the AI’s own output with zero trust . Neither the model nor a person ever has to take a clause on faith. They can see, per item, exactly where automation can be relied upon and exactly where a human should look. And this is where Human-in-the-loop actually lives. It isn’t an abstract principle — it’s a flag in a table that says, for each piece of data, “from here, a person decides.” For a Legal Operations tool, that line — drawn by the system, in data, between what’s safely automated and what a person (and where appropriate, the legal team) should review — is the whole point. The trade-off. The cost is a deterministic audit pass over every clause, paid up front — and it’s a bargain. In return you get two things at once. You structurally prevent the kind of undetected error that no after-the-fact warning can ever catch; and every output now carries an explicit, machine-readable trust boundary. A risk that used to be unknowable — contamination that raises no error of its own — becomes a known, bounded property of the data that humans and agents alike can act on. Decision 5 — Match the pattern to the nature of the data. Reject the universal solution. This is the heart of the tour. Not all data is the same shape, and the single biggest architectural mistake is pretending it is — trying to serve everything through one generic file-reading tool. ContractOps recognizes three fundamentally different kinds of data and applies a different decomposition to each. A. Settled data — already in the system. Past, executed contracts that have been through the ETL pipeline. The comprehension was done up front, so the runtime tooling is lean: scan the lightweight index, then fetch by ID. This is the world of Decisions 2–4. B. Just-arrived data — handed over in the moment. A draft a user just dropped into the chat; a working spreadsheet that didn’t exist five minutes ago. There’s no pre-processing something you’ve never seen, so here the parsing does happen at runtime — but carefully. A heavyweight, isolated tool reconstructs the document into structured Markdown via vision reasoning, then returns just the list of section headers  — a table of contents that doubles as a schema. The moment those headers exist, the model is no longer wandering an unknown document; it’s choosing from a map. It then issues one of two deterministic requests: fetch a whole section, or pull only the blocks matching a condition. The tool runs the repetitive search internally and answers in one shot —  declarative extraction , instead of the model groping item by item. C. Opaque external data — out in the wild (think an untamed Google Drive: folders of mixed PDFs, scans, and docs nobody curated). Files scattered across storage you don’t control and can’t pre-index, where the search API hands you discovery and filtering as a single inseparable operation. Here I keep “finding” fused but force “fetching” to stay separate and lazy  — never opening a file until the model has committed to it. The point isn’t the taxonomy for its own sake. It’s the discipline: a tool is not a one-size-fits-all pipe. Choosing the decomposition that fits the data’s actual structure — its maturity, its time constraints, its opacity — is the design work that keeps the model out of trouble. Themis & supervisor agents flow (interactive + monitoring) The interactive side: Themis reasons over pre-structured knowledge, while the supervisor agents watch the same source of truth from different angles. Decision 6 — Choose an honest limit over false confidence. The universal problem. When the agent searches opaque external storage and hits a file it can’t parse — a legacy Office document, an unreadable image — the tempting design is to filter it out of the results. And even when no one decides this on purpose, simply wiring up generic tools tends to produce exactly that exclusion by default. Cleaner, right? Except now the agent sees nothing where something exists, concludes “no relevant information found,” and reports that with total confidence. A confident false negative  — “that document doesn’t exist” when it very much does — is not just a bug; it is an architectural betrayal. The approach. Don’t hide the unprocessable. The search results include the file even if it can’t be opened. The block happens later, at fetch time, where the tool refuses the unsupported format and says so : “a relevant file exists here, but the system can’t process this format — please check the source directly.” The model stays fully aware the file exists, and instead of inventing an absence, it takes the honest path: it escalates to a person with a precise pointer. This is the point that’s easy to get backwards. When the system honestly reports what it cannot read, it makes the human’s proper role possible: you go to the source and, where it matters, cleanse or contextualize that file by hand before handing it back. On honest information, that isn’t a workaround — it’s the legitimate division of labor between a person and a tool. Real collaboration is built on understood constraints, not on the pretense of unlimited capability. Decision 7 — Govern by choreography, not by a conductor. This is the technical centerpiece, and it’s where the serverless substrate really earns its keep. It’s also where ContractOps does its most valuable supportive work: keeping the shared picture fresh. The universal problem. The default way to coordinate many agents is to appoint a central orchestrator and run everything through an external message queue. It works, but it buys you a single coordination point to keep alive, state duplicated between the queue and your database, and a whole category of distributed-systems bugs — most painfully, notifications that drop or double-fire with nothing to flag either. For supportive monitoring, where the entire value is keeping everyone in sync , a missed or duplicated notification is exactly what you can’t have. The approach, in three parts. A. Choreography over orchestration. There is no conductor. The monitoring agents —  Argus (contract changes), Chronos (deadlines and milestones), Cassandra (early warnings when activity looks likely to run against the terms) — are independent, each woken by its own EventBridge schedule or event. They never call a central brain. Instead, each watches the same single source of truth  — the DynamoDB state — from its own angle. Good coordination emerges from several independent observers cross-checking one shared reality, the way a well-run team stays aligned precisely because more than one person is paying attention. This is also how the supportive jobs map cleanly onto the lifecycle: Argus helps the latest contract state reach the right people; Chronos surfaces milestones early enough to plan around; Cassandra raises an early warning when planned human activity looks likely to run against the schedule or the agreed terms. B. Pure-state idempotency, with no external queue. This is the part I’m proudest of. No SQS, no broker — just Lambda and DynamoDB. Coordination is achieved through DynamoDB conditional writes (optimistic concurrency) plus disciplined state management. Each agent, before it acts, checks the current state and skips if it isn’t what it expected. The result is a fully idempotent “singleton” flow whose design mantra is: a notification is never missed and never duplicated. Those are the two failure modes that plague every naive notifier, and they’re closed off purely through state, not queue plumbing. C. Self-healing on the assumption of undetected failure. Distributed systems fail in ways that raise no error, so the design assumes it. Every Scribe, whenever it runs, also sweeps for stalled jobs  — anything stuck past a short threshold — and resets them to the start of the pipeline. I call it the Ride-Along Sweep : an agent’s ordinary execution doubles as a recovery pass for the stuck work of the colleagues it collaborates with , so a group of collaborators heals itself. Clio runs a full-fleet sweep on a daily schedule as a backstop. The pipeline doesn’t promise it never stumbles; it guarantees it never stays down . A boundary worth naming. Note precisely what these agents do: they reconcile facts and raise notifications. They do not make legal calls. The monitoring fleet hardens the factual groundwork that a person then judges — which is exactly the line a Legal Operations tool must hold. The trade-off. You accept eventual consistency and a little sweep latency. In return you get operational simplicity, low cost, and a kind of fault tolerance that a queue-centric design would charge you dearly for. Decision 8 — Keep secrets inside the boundary. Hold no keys. Contract information is confidential, which makes security its own design pillar — and another place the AWS foundation does heavy lifting. The universal problem. Two fears dominate: that confidential data slips out to a public service or an external AI through some unexpected egress, and that a static credential leaks and hands an attacker the keys. The approach. A private, enclosed boundary. Model inference (Bedrock) and the core retrieval stay inside AWS . Only the strictly necessary paths out — to Google Drive, to Slack — are opened, each behind a tightly scoped API. Unexpected egress is designed out, not merely monitored. Data is encrypted at rest with AWS KMS , and the handful of secrets that do exist live in AWS Secrets Manager  — never in code or environment dumps. Keyless security via Workload Identity Federation. There are no static cloud credentials to leak, because there are none. Starting from an AWS IAM Role , the system federates into Google Cloud service accounts by exchanging short-lived tokens on demand — a multi-cloud privilege chain with nothing long-lived to steal. Dual service-account partitioning. The service account that parses documents and the one that searches are strictly separated, so least privilege is enforced at the infrastructure layer, not by convention. Layered Slack access control. Three gates in series: AWS WAF at the perimeter, Slack request-signature verification for authenticity, and a channel-ID allowlist for authorization — logically sealing the public endpoint down to specific channels only. The throughline is the same one running under every decision in this article: explicit limitation. A boundary you can name and enforce beats a capability you merely hope behaves. The philosophy underneath: autonomy through constraint Step back, and every decision rhymes. Each one takes something away from the model  — the right to roam the physical layer, the burden of guessing at structure it was never told about, the temptation to answer when it shouldn’t — and hands that responsibility to a part of the system actually equipped to bear it. What’s left for the LLM is the thing it’s genuinely good at: reasoning over clean, trustworthy, bounded options. That turned out to be a paradigm worth formalizing. The grand version of the idea — why these boundaries are not just convenient but necessary , and how to reason about them systematically — is laid out in a preprint I’ve written: Survey–Sonar–Pickup (SSP) . ContractOps is its living proof, but the principles are meant to outlive any one system. And the principle, in the end, is a humble one. Real autonomy — and real collaboration between people and AI — doesn’t come from dogmatically worshiping an LLM’s unconstrained capabilities. It comes from ruthlessly enforcing honest, structured boundaries, so the system knows its own limits and a person always holds the decisions that matter. For a tool that supports Legal Operations — work that belongs to people, backed by their legal team — that isn’t a compromise. It’s the entire point. It’s also what lets everyone involved, on both sides of a contract, get on with fulfilling it in confidence. The architectural paradigm behind these decisions — Survey–Sonar–Pickup (SSP) — is described in a preprint: [ The Survey-Sonar-Pickup (SSP) Paradigm: Redefining Responsibility Boundaries Between LLMs and Physical Data Layer ] . ContractOps runs fully serverless on AWS (Lambda, DynamoDB, Bedrock, EventBridge, S3). If you’re building agents that work with messy, high-stakes information, I hope a few of these boundaries are worth borrowing. Special Thanks: By the way, the awesome agent icons used throughout ContractOps (and in the architecture diagram here) were generated by Gemini (nano-banana). In the spirit of full transparency — and recognizing the irony of writing an entire article about strictly limiting AI, only to fully outsource the graphic design to it — huge thanks to the model for bringing these agents to life! ContractOps: A Serverless Multi-Agent System for Contract Operations was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
G-gen の西田です。ノーコードで Google Workspace のタスクを自動化できる Google Workspace Studio に導入された、リストデータを繰り返し処理できる ループ処理機能 について解説します。 はじめに Google Workspace Studio とは ループ処理機能の概要 検証シナリオ 設定手順 動作確認 はじめに Google Workspace Studio とは Google Workspace Studio とは、プログラミングを行うことなく、Google Workspace の各アプリケーションや Gemini を組み合わせた自動化フローを作成できる AI エージェントツール です。 ユーザーは「ステップ」と呼ばれる個々のタスクを順に配置することで、複雑な業務プロセスを自動化できます。例えば、「メールを受信したら Gemini で内容を要約し、Google Chat に投稿する」といったフローを直感的に構築可能です。 Google Workspace Studio の詳細やコンセプトについては、以下の記事を参照してください。 blog.g-gen.co.jp ループ処理機能の概要 Google Workspace Studio のフローにおいて、2026年6月に リストをループ処理する機能 が導入されました。これにより、リスト形式のデータに対して同じ処理を繰り返し実行できるようになりました。これまでは1つのデータに対して1つの処理を行うフローが中心でしたが、今回のアップデートにより、リストデータを一括で処理する高度な自動化が容易になります。導入された機能は主に以下の2つです。 機能 概要 Ask Gemini のリスト出力 「Ask Gemini」ステップの「Response format」設定に リスト形式 が追加されました。Gemini に相談した結果を構造化されたリストとして後続のステップに渡すことができます。 Repeat for each Gemini に相談した結果のリストの各項目や Google スプレッドシートの各行に対して、指定した一連の処理(サブステップ)を 繰り返し実行 するステップです。 参考 : Introducing the ability to loop over a list of items in Workspace Studio - Google Workspace Updates 検証シナリオ ループ機能の具体的な活用例として、社内パソコンの管理台帳(Google スプレッドシート)を参照し、OS のサポートが切れている端末の利用者へ案内メールの下書きを作成するフローを構築します。 検証用の管理台帳には、端末管理ソフト等で取得できるデータの一部である、「端末 ID」「マシンベンダー」「OS」「最終起動日」「ログイン ID」「メールアドレス」の項目を持つリストを使用します。 フローでは以下の処理を自動化します。 Gemini で案内メールのテンプレートを作成 管理台帳からデータを取得 各行の OS 情報をチェックし、サポート切れの場合はメールの下書きを作成 管理台帳の「ステータス」列を Done に更新 設定手順 Google Workspace Studio で以下のステップを持つフローを作成します。 ステップ1: トリガー設定 フローが実行されるトリガーを設定します。今回は特定の日時に開始するように設定します。 ステップ2: メール文面作成 サポート切れのパソコンの利用者に向けた案内メールの文面を作成します。検証では「Gemini に相談」を利用して文章を作成します。 なお本来、ここで指定するメールの文面は定型文で問題ありませんが、当記事では Gemini による生成機能の説明を目的として「Gemini に相談」を使用しました。 ステップ3: リストデータ取得 「シートのコンテンツを取得」アクションを選択し、対象のスプレッドシートのシートからリストデータを取得します。すべての行を取得するため「値を取得する行を検索」では、「すべて」を指定します。 ステップ4: ループ処理開始 ループ処理を設定します。日本語の画面では、「それぞれについて繰り返す」というツールを選択します。 「リストを選択します」の変数ボタンから、ステップ3で取得したシートのコンテンツを選択し、「詳細リスト - 一致する値」を指定します。 ステップ5: 処理対象か確認 ループ内のサブステップとして、「OS がサポート切れであるか」を判定するロジックを記述するため、AI アクションの「決定」を追加します。 処理行の「OS」の値を取得するため、プロンプト入力欄の変数ボタンから、ステップ4のループ処理を選択し、その中から「一致する値 "OS" の各アイテム」を指定します。 ここでは、「処理行の OS がサポート切れである」という条件から真偽値を判定させます。 ステップ6: 条件分岐 条件分岐のステップでは、ステップ5の結果が "真" である場合、後続のステップに進ませるようにします。 ステップ7: 下書き作成 条件に一致した行の "メールアドレス" 宛のメールの下書きを作成します。宛先のメールアドレスは、ステップ4のループ処理の中から「一致する値 "メールアドレス" の各アイテム」を指定します。メッセージ(本文)は、ステップ2で作成した文面を指定します。 ステップ8: 台帳更新 下書きを作成した行については、作成済みの記録を入力する処理を設定するため「行を更新」というステップを選択します。更新対象のスプレッドシート、シート、対象の行および入力する値をそれぞれ指定します。 動作確認 フローをテスト実行すると、管理台帳の各行が順番に処理されます。 管理台帳を確認すると、Windows 11 の行以外については、サポート切れ OS と判定され、G 列に下書きが作成された記録が入力されています。 また、作成された下書きメールは以下の通りです。今回の管理台帳では対象となる行が45行ありましたので、同様の下書きメールも45件作成されました。 なお、ループ処理できる行の数は画面の注意書きに記載の通り、100件までです。 101件目以降のデータを処理する場合は、スプレッドシートのシートからのリストデータ取得時に条件を加えるなどして調整します。上記の手順では、ステップ3の「シートのコンテンツを取得」のステップに、「下書き作成」が空白であるといった抽出条件を加えます。 西田 匡志 (記事一覧) クラウドソリューション部 美容商社→物流→情シスを経て、2025年6月G-genにジョイン。Google Cloud を通じて多くの人に貢献できるよう日々精進!
こんにちは。Data Ingestion チームでData Engineerをしている @orfeon です。この記事は「 Merpay & Mercoin Tech Openness Month 2026 」の14日目の記事です。 はじめに Data Ingestion(旧Data Platform)チームでは、多数のマイクロサービスが管理する データベース・テーブル から、大量のデータを継続的にDWH(データウェアハウス)へ同期する必要があります。同期対象には数億〜 数百億件 に達する大規模なテーブルも含まれ、これらをいかに速く・安全に・一貫性を保ったまま抽出するかが、DWHの鮮度や安定性にとって大事になります。 これまで  Cloud Spanner  からのデータ取得では、Spannerの分散DB特有の機能(後述)を活用することで、大規模テーブルでも高いスループットでの取得を実現できていました。 一方、社内にはTiDBやAlloyDBといったSpanner以外のデータベースも多く利用されており、その中には数百億件以上に達するテーブルもあります。 これらのテーブルは従来、主キーなどで シーク方式 で取得していましたが、単一コネクションでの シーケンシャルなデータ取得 になるため、大規模テーブルでは取得に非常に時間がかかっていました。 そこで今回、Spannerと同じように、 それぞれのDBに特有の機能を活用して並列取得などでスループットを上げる よう工夫しました。 具体的には、 TiDB  と  AlloyDB  の大規模テーブルをDWHへ同期する仕組みを  Cloud Dataflow(Apache Beam)  上に構築しました。 本記事では、その中核となる2つのSourceモジュール   TiDBSource  と  PostgresSource   について、高いスループットを実現するための工夫を解説します。 なぜ汎用JDBCではなく専用モジュールなのか Beam/Dataflowには汎用的な  JdbcIO  が既に存在します。 しかし汎用JDBCは「 SELECT を実行して結果を1行ずつ読む」という標準的な経路をたどるため、大規模テーブルでは以下のボトルネックが発生します。 1行ごとのSQL処理オーバーヘッド : 通常のクエリ実行では、サーバ側でのタプルのテキスト/プロトコル変換などが行ごとに発生する。 並列化の難しさ : テーブルを並列に読むには「どこで分割するか」を決める必要があるが、 OFFSET ベースの分割はオフセットが大きくなるほど遅くなり、フルスキャンを誘発する。 一貫性の確保 : 並列に複数コネクションから読む場合、各コネクションが別々の時点を読むと整合性が崩れる。 そこで今回のモジュールでは、 それぞれのデータベースが持つネイティブなバルク転送機構と物理的なデータ配置情報を活用 し、汎用JDBCのボトルネックを回避する設計にしました。 加えて運用上の大きなメリットとして  分割キー(フィルタ条件)の自動抽出  があります。 マイクロサービスごとに膨大なテーブルを扱う環境では、テーブル1つひとつに対して「どのカラムで分割するか」を人手で指定するのは現実的ではありません。 両モジュールはテーブルのメタデータから 主キー(PK)や暗黙の行ID、物理ブロック位置を自動で見つけ出し 、分割範囲の絞り込み条件を組み立てます。 利用者は接続先とテーブル名を指定するだけで、同じ設定が多数のテーブルに横展開することができます。 なぜCloud Spannerでは高いスループットでデータ取得が可能なのか 今回の設計の発想は、既にうまくいっていたSpannerからの取得方法を、TiDBやAlloyDBにも持ち込むことにありました。 そこでまずSpannerが大規模テーブルでも高いスループットを出せている理由を説明します。 Spannerは分散データベースとして、以下の機能を組み合わせています。 PartitionQuery / PartitionRead(Splitベースの自動分割) : Spannerはデータを内部的に  Split (キー範囲+負荷ベース)へ分割して保持しています。 PartitionQuery  はこのSplit境界に基づいてクエリを複数のパーティションに自動分割します。クライアントはキー範囲などSplitの内部構造を意識する必要がありません。 BatchReadOnlyTransaction(スナップショット一貫性) : 全パーティションの読み取りが、 TimestampBound  で指定した同一スナップショットを参照することを保証します。ロックを取らずに一貫した読み取りができます。 Partition Tokenの分散・並列実行 : 分割結果は シリアライズ可能なPartition Token として返されるため、複数プロセス・複数マシン、そして Beam Worker に配布してそのまま並列実行できます。Apache Beamの  SpannerIO  も内部でこの仕組みを使っています。 Partition Tokenによる自動バージョン保持 : Tokenが有効な間は対象バージョンがGCされないことが保証されるため、クライアント側で明示的なバージョン保護(SafePoint管理)が不要です。 Data Boost(Spanner固有) : Google管理の独立した計算リソースで読み取るオプションで、 本番ワークロードへの影響をほぼゼロ にしつつ弾力的にスケールできます。 これらは「 物理的なデータ配置に沿った自動分割 」「 スナップショット一貫性 」「 分割単位の分散ワーカーへの配布と並列実行 」という構図で成り立っています。Spannerではこれらが高度に抽象化されたAPIとして提供されていますが、 TiDBやAlloyDB(PostgreSQL)にもそれに近いDB固有の機能が存在します 。 このSpannerの機能とTiDBやPostgreSQLの機能は以下のように対応します。 Spanner TiDB(dumpling相当) AlloyDB(PostgreSQL) PartitionQuery(Split境界で自動分割) TABLESAMPLE REGIONS() (TiKV Region境界) ctid 物理ブロック範囲( pg_relation_size ) BatchReadOnlyTransaction(スナップショット) tidb_snapshot MVCC(Multi-Version Concurrency Control) + TSO(Timestamp Oracle) バッチ読み取り( ctid スナップショットのズレは許容) Partition Tokenの分散実行 Range条件の分散実行(本記事の設計) Range条件の分散実行(本記事の設計) Partition Tokenによる自動GC保護 tidb_gc_life_time  の引き上げで代替 (該当なし) SpannerのSpannerIOで提供されている「分割 → 配布 → 並列スナップショット読み取り」を、TiDB/AlloyDBではDB固有の機能を組み合わせて自前で構築する、というのが本記事のモジュールの狙いです。 以降その共通の仕組みと各DB向けの実装を見ていきます。 共通アーキテクチャ 両モジュールに共通する基本戦略は次の3ステップです。  TiDBSource / PostgresSource はCloud Dataflow バッチジョブとして実行され、以下3つのステップで役割が分かれます。 テーブルの範囲分割 : 1本のコネクションでメタデータだけを取得し、テーブルを物理的な分割単位(Range)のリストに列挙する 再分配 : 分割単位をPCollectionの「種」として撒き、 Reshuffle でワーカーに再分配する 並列読み込み : 各ワーカーが担当Rangeをネイティブのバルク転送機構で並列に読み取る 以降、TiDBとPostgreSQLそれぞれについて、この3ステップの中身を掘り下げます。まずTiDBから、この3つのステップがどのように実装されるかを見ていきます。 TiDBテーブルからのデータ抽出 TiDB公式ツール dumpling に学ぶ TiDBには  dumpling  という高速なエクスポートツールが公式に提供されています。 TiDBSource の設計は、このdumplingが高スループットを実現している仕組みを参考にしています。 まずはdumpling側の要点を整理します。 テーブルのチャンク分割と並列読み取り dumplingは、1テーブルを丸ごと1クエリで読むのではなく、テーブルをチャンク(範囲)に分割し、各チャンクを独立したSELECTクエリとして並列実行します。 チャンク分割は3段階のフォールバック構造になっています。 戦略 方式 概要 A(最優先) TiKV Regionベース分割 TABLESAMPLE REGIONS()  でRegion境界をチャンク境界にする B(フォールバック) 数値インデックスベース分割 数値型PK/インデックスのMIN/MAXから均等分割 C(最終) テーブル全体ダンプ 分割可能なフィールドがない場合は1クエリ 特に重要なのが戦略Aです。 TiDBではデータがTiKV上で Region(デフォルト96MB単位) に分散配置されます。 dumplingはこのRegion境界をそのままチャンク境界として利用するため、各チャンクが異なるTiKVノードへの読み取りリクエストに分散され、クラスタ全体のI/O帯域を引き出せます。 dumplingの並列実行の仕組み: Producer-Consumer 分割したチャンクを並列に読み出すために、dumplingは内部で Producer-Consumer という構造をとります。登場人物は次の3つです(いずれもdumplingの実装に出てくる用語です)。 Producer(プロデューサ) : テーブルをチャンクに分割し、「このチャンクを読め」というタスクを作り続ける係。dumplingではメインのgoroutineが担当します。先ほどのRegion境界などをもとにタスクを生成します。 Writer(ライター) : 生成されたタスクを受け取り、実際にSELECTを発行してデータを読み出す係。 --threads で指定した数だけ並列に動き、それぞれが独立したDB接続を持ちます。タスクを消費するConsumer側にあたります。 infiniteChan(無制限チャネル) : ProducerとWriterの間をつなぐ、容量に上限のないキュー(待ち行列)。Writerの処理が詰まってもProducerがブロックされず、生成済みのタスクをいくらでも貯めておけます。 このように、タスクを作成する人(Producer)とタスクを実行する人(Writer)を分離し、その間を待ち行列(infiniteChan)でつなぐことで、分割と読み取りを互いに待たせずに並列で回す基本構造です。後述のTiDBSourceは、この役割分担をそのままDataflowの分散モデルに置き換えています。 Snapshot読み取り dumplingはTiDBのMVCC (Multi-Version Concurrency Control)機構を利用し、特定のTSO(Timestamp Oracle)時点の  スナップショット  から一貫したデータを読み取ります。 ロック不要 :  FLUSH TABLES WITH READ LOCK  のような排他ロックが不要で、書き込みをブロックしない。 一貫性保証 : 全Writerが同一時点のデータを読むため整合性が保たれる。 高スループット : ロック競合がないため並列度を上げられる。 加えてdumplingは、長時間のダンプ中にTiDBのGC(Garbage Collection)がスナップショット時点の古いバージョンを回収しないよう、PD(Placement Driver)に対してGC SafePointを登録します。 TiDBの機能を活用したDataflowでの実装 TiDBSource は、dumplingのこれらのアイデアを Apache Beam / Dataflowのモデルに移植 したものです。dumplingがgoroutineで実現していた並列性を、Dataflowの分散ワーカーによる並列性に置き換えています。対応関係は次の通りです。 dumpling TiDBSource (Dataflow) Producerがチャンクタスクを生成 パイプライン構築時に Range のリストを生成 infiniteChan  + 複数Writer goroutine Reshuffle  +  ParDo による分散ワーカー並列処理 各Writerが独立DB接続でSELECT 各ワーカーが @Setup で独自コネクションを確立 TSOスナップショット読み取り TSOを一度取得し全ワーカーに配布 ステップ1: 分割キーの決定とRangeの列挙 パイプラインの初期起動時に1本のコネクションを張り、出力スキーマの確定・スナップショットTSOの取得・テーブルの分割を行います。 ここではメタデータと境界値だけを読み、実データのスキャンは行いません。 分割キーの自動解決  は次の優先順位で行われます。利用者がカラムを指定しなくても、テーブルのメタデータから自動的に決定されます。 利用者が  splitField  を明示指定していればそれを使う なければ 単一カラムの主キー(PK) それもなければ 暗黙の行ID  _tidb_rowid (クラスタードキーを持たないテーブル向け) _tidb_rowid  は、明示的な主キーを持たないテーブルでTiDBが内部的に振る暗黙の行IDです。 これを分割キーに使えるため、主キー設計に依存せず、どんなテーブルでも分割の足がかりを得られます。 Rangeの列挙  は、先述のdumplingの戦略A→B→Cと同じ3段階フォールバックで行います。 戦略Aは、次のSQLでTiKVのRegion境界を取り出します。 SELECT `pk` FROM table TABLESAMPLE REGIONS() ORDER BY `pk` TABLESAMPLE REGIONS() は各Regionの先頭行を返すため、結果の各値が「次のチャンクの下限」になります。 境界値の列 b[1], b[2], …, b[n] から、隣り合う境界で挟まれた半開区間を生成します。 取りこぼしを防ぐため、最初の区間は下側を、最後の区間は上側を開いておきます。 chunk[ -∞, b[1] ), chunk[ b[1], b[2] ), …, chunk[ b[n], +∞ ) TABLESAMPLE REGIONS() はTiDB v5.0以降の構文です。 非TiDBのMySQLや古いTiDBではこのクエリが失敗するため、自動的に戦略B(数値MIN/MAX均等分割)へフォールバックします。 戦略Bは、SELECT MIN(pk), MAX(pk) で取得した範囲を、推定行数とチャンクあたりの目標行数 splitSize から決めた個数で等分します。 chunks = ⌈ 推定行数 / splitSize ⌉ step = (max − min) / chunks + 1 区間 = [min, min+step), [min+step, min+2·step), … , [ …, max] (stepの計算では厳密な切り上げ ⌈(max−min)/chunks⌉ ではなく+1 としています。半開区間 [cutoff, cutoff+step) で走査するため、割り切れるケースでもmax が最終チャンクに確実に含まれるようstep を 1 大きく取っており、実際のチャンク数は chunks 以下になります) ステップ2: Rangeの再分配(Reshuffle) 範囲が決まったら次にワーカーに範囲ごとの処理を並列にさせる必要があります。列挙した Range のリストを並列実行するよう明示的に指定するためにPCollection化した Range の後に、 Reshuffle.viaRandomKey()  を挟みます。  Reshuffle  には2つの重要な役割があります。 fusionの分断 : Dataflowは連続する処理を一つの処理Stageとして結合してしまうことがあります。これがないとDataflowは Create と後続の読み取り ParDo を融合し、Rangeが1ワーカーに偏って並列性が出ません。 ランダムキーによる再分配 : 全Rangeが利用可能なワーカーへ均等にばらまかれ、クラスタ全体に読み取り負荷が分散されます。 これがdumplingの「 infiniteChan から複数Writerがタスクを取り出す」構造に相当します。 ステップ3: 各ワーカーでのスナップショット並列読み取り 各ワーカーは処理開始時( @Setup )に自前のコネクションを確立し、パイプライン構築時に取得・配布されたTSOで  tidb_snapshot  をセットします。  全ワーカーが同一TSOを使うことで、分散読み取りでも単一時点の一貫したスナップショットになります 。 TSOの取得は、一貫スナップショットを開始してその時点の論理時刻を読むだけで完了します(トランザクション自体はすぐロールバックします)。 START TRANSACTION WITH CONSISTENT SNAPSHOT;SELECT @@tidb_current_ts; -- ← この値(TSO)を全ワーカーに配布 各ワーカー側では、読み取り前にセッション変数を設定します。 SET @@tidb_snapshot = '<配布されたTSO>'; -- ロックなしで同一MVCC版を読む SET @@session.tidb_enable_paging = ON; -- 大量スキャン時のメモリ使用量を抑制 tidb_enable_paging  はCoprocessorリクエストのメモリ使用量を抑える設定です(TiDB v6.2.0以降はデフォルト有効。変数を知らないDBではスキップ)。 実際の読み取りでは、担当Rangeを分割キーへの 値の範囲条件 に変換してSELECTに組み込みます。 SELECT * FROM table WHERE `pk` >= 1000 AND `pk` < 2000 ここで重要なのは、この条件が 主キー(インデックス)に対する値の比較 である点です。 データベースはこの行値比較を使ってインデックスを シーク し、該当範囲の先頭へ直接ジャンプして必要な行だけを読みます。  OFFSET のように先頭から数え直す必要がないため、どのチャンクも一定コストで読み出せます。 また、読み取りはMySQL Connector/Jの 行単位ストリーミングモード ( fetchSize = Integer.MIN_VALUE )で行います。 これは結果セット全体をワーカー側メモリにバッファせず1行ずつ取り出す特別な設定で、巨大チャンクでもワーカーのメモリ消費が一定に保たれます。 dumplingがgo-sql-driver/mysqlのストリーミング動作で実現しているのと同じ効果を、JDBC側で引き出している形です。 制約: GC SafePoint dumplingはPDクライアント経由でGC SafePointを登録しますが、これは JDBCコネクションからは到達できません。 そのためTiDBSourceではSafePoint登録を行わず、長時間の読み取りに対しては運用側でクラスタの tidb_gc_life_time を引き上げ、読み取り中にGCがスナップショットのバージョンを回収しないようにする運用方針としています。 AlloyDB(PostgreSQL)テーブルからのデータ抽出 AlloyDBとPostgreSQL互換性 AlloyDBは、Google CloudがPostgreSQL互換で提供するフルマネージドのデータベースサービスです。  ワイヤプロトコルもSQLの方言もPostgreSQLと互換 であるため、クライアントから見ればPostgreSQLそのものとして扱え、標準のPostgreSQL JDBCドライバがそのまま使えます。 さらに  ctid (物理行位置)や  COPY 、 pg_relation_size  といったPostgreSQLの内部機能・システムカタログも利用できます。 したがって、AlloyDBからのデータ抽出は  PostgreSQLの機能を前提に設計できます 。 以下では PostgresSource がPostgreSQLのどの機能を使っているかを説明しますが、その内容は基本的にAlloyDBにも当てはまります。 通常のJDBCクエリ取得との違い PostgresSource も「物理的な分割 → 並列読み取り」という骨格はTiDBと同じですが、データ転送経路とテーブル分割の方式 がPostgreSQL固有の機能に最適化されています。 まず、通常のJDBC経由のクエリ取得との違いを整理します。 観点 通常のJDBC ( SELECT ) PostgresSource ( COPY ... TO STDOUT BINARY ) 転送経路 拡張クエリプロトコル COPYプロトコル(バルク転送専用) 行ごとの処理 パース/プラン/結果整形が走りうる クエリは1回プラン。あとはタプルを連続送出 データ形式 テキスト or バイナリのフィールド単位 バイナリのタプルストリームを直接デコード 並列分割 OFFSET / LIMIT 等(大きいほど低速) ctid 物理ブロック範囲(フルスキャン不要) PostgreSQLの COPY は、もともと大量データのインポート/エクスポートのために用意された専用経路で、通常のクエリ実行に伴う1行ごとのオーバーヘッドを回避できます。 PostgresSource はJDBCドライバの CopyManager API(PGCopyInputStream)を使い、COPY (SELECT …) TO STDOUT (FORMAT BINARY) のバイナリ出力ストリームを受け取って、Avroのレコードへ直接デコードします。 中間のテキスト変換を挟まないぶん、CPU負荷とアロケーションを抑えられます。 PostgreSQLの機能を活用したDataflowでの実装 ステップ1: ctidブロック範囲の列挙 PostgreSQLの全タプルには ctid(物理的な行ロケーション = (ブロック番号, ブロック内オフセット))が付与されています。 PostgresSource はテーブルを 物理ブロック範囲 で分割し、各範囲を TID range scan で読みます。 分割の計画には実データのスキャンが一切不要です。 ブロック数は、テーブルの実ディスクサイズをブロックサイズで割って求めます。 SELECT pg_relation_size('table'::regclass) / current_setting('block_size')::bigint pg_relation_size  は実ディスクサイズを返すため、統計情報の  pg_class.relpages  推定値よりも正確で、しかもstat呼び出し1回で済みます。 1ブロックあたりの行密度は推定行数( pg_class.reltuples )から求め、目標行数  splitSize  に対応するブロック幅を機械的に算出します。  [0, blockCount)  をこの幅で割っていくだけなので、 フルスキャンも OFFSET も不要 です。 行密度 = 推定行数 / ブロック数1範囲のブロック幅 = max(1, round( splitSize / 行密度 )) 範囲 = [0, w), [w, 2w), … , [kw, blockCount) ※最後の範囲は上限を開く 最後の範囲を上限なし(オープンエンド)にしておくことで、分割を計画した後に追記された行も読み取れます。各範囲は ctid の半開区間条件に変換されます。 WHERE ctid >= '(0,0)'::tid AND ctid < '(3,0)'::tid この条件も、TiDBの主キー範囲と同様に物理位置の値による範囲比較です。 PostgreSQL 14以降ではこれが TID range scan として処理され、該当ブロックへ直接シークして必要な範囲だけを読みます。 注意点 :  ctid  は物理的な行位置なので、読み取り中にINSERT/UPDATE(別ページへの移動)/VACUUMが起きると、同じ行を二重に読んだり取りこぼしたりする可能性があります。同期中に更新されないテーブルに対して使うか、バッチ読み取りで一般的なスナップショットのズレを許容する前提で利用します。また、TID range scanはPostgreSQL 14以降のサポートで、それ以前のバージョンでは各範囲がシーケンシャルスキャンにフォールバックします。 ステップ2: Rangeの再分配 TiDBと同様、RangeのリストをCreateで撒き、Reshuffle.viaRandomKey()でワーカーへ再分配します。fusion分断と負荷分散の狙いは同じです。 ステップ3: 各ワーカーでのバイナリCOPY並列読み取り 各ワーカーは、担当Rangeのctid条件を組み込んだSELECTを COPY (…) TO STDOUT (FORMAT BINARY) でラップし、バイナリストリームを受け取ります。 COPY (SELECT * FROM table WHERE ctid >= '(0,0)'::tid AND ctid < '(3,0)'::tid)TO STDOUT (FORMAT BINARY) 返ってくるのはPostgreSQLのCOPYバイナリフォーマットです。 PostgresSource はこれを直接パースします。 先頭の固定シグネチャ( PGCOPY\n\377\r\n\0 )とヘッダを読み飛ばし、以降はタプルを1件ずつ読みます。 各タプルは「フィールド数」に続いて、フィールドごとに「長さ( -1 はNULL)+値のバイト列」が並ぶ構造で、終端は番兵値(フィールド数 =  -1 )で示されます。 値のデコードは、PostgreSQLのバイナリ表現をAvroの値へ型ごとに変換します。たとえば次のような処理です。 整数・浮動小数: ビッグエンディアンでそのまま読む numeric : base-10000 のdigit配列から十進数を復元 date  /  timestamp : PostgreSQLエポック(2000-01-01)基準の値をUnixエポック基準へ補正 uuid  /  json  /  jsonb  /  bytea : それぞれの専用処理 これらをドライバのテキスト変換を介さず自前でデコードすることで、転送経路を最短化しています。 なお、IAM認証(Cloud SQL / AlloyDB)にも対応しており、 user 未指定時はワーカーのサービスアカウントをDBユーザとして使い、接続URLに  enableIamAuth=true  を自動付与します。 検証用の12つのカラムを持つ6億件のダミーテーブルデータをAvroファイルとしてGCSに出力するタスクで、6コア並列で8分で処理完了するようになりました。 制約: なぜTiDBSourceのようにワーカー間の一貫性を担保できないのか TiDBSource では tidb_snapshot によって全ワーカーが同一時点を読み、ワーカー間の一貫性を担保していました。一方の PostgresSource では、各 ctid レンジが それぞれ独立したトランザクションで読まれる ため、レンジ間(別接続・別時刻)での一貫性は保証されません。読み取り中にINSERT/UPDATE( ctid が別ページへ移動)/VACUUMが起きると、レンジをまたいで行の重複や欠落が起こりえます。 PostgreSQLにも一見すると同等の仕組みがあります。 pg_dump の並列モードは、 pg_export_snapshot() でスナップショットをエクスポートし、各ワーカーが SET TRANSACTION SNAPSHOT でそれを取り込むことで、ワーカー間の一貫性を担保しています。 PostgresSource で同じことを今回実現できなかった理由は スナップショットの「寿命」の違い にあります。 TiDB( tidb_snapshot ) PostgreSQL( pg_export_snapshot() ) 実体 TSO(論理タイムスタンプ=ただの数値) エクスポート元トランザクションに紐づくスナップショットID 寿命 永続的 (GCされるまで。トランザクション非依存) 一時的 (エクスポート元トランザクションが開いている間だけ有効) 共有方法 数値を渡し、各セッションが独立に SET 元トランザクションが生存中に各セッションが SET TRANSACTION SNAPSHOT で取り込む PostgreSQLのエクスポートされたスナップショットは、 それをエクスポートしたトランザクションが終了するまでしかインポートできません 。 pg_dump の並列モードが成立するのは、単一プロセスのリーダーがダンプ全体の間ずっとエクスポート元トランザクションを開いたまま保持し、自身でワーカーを起動するからです。 PostgresSource が動くCloud Dataflowの実行モデルでは、元トランザクションを 並列読み取りの全期間にわたって保持する場所を確保するのが難しかったのです 。TiDBSourceではランチャーでTSO(数値)がトランザクションと無関係に有効だったためワーカーに配るだけで済みました。 そのため PostgresSource では各レンジ独立読み取りを前提とし、「同期中に更新されないテーブルに対して使う/バッチ読み取りで一般的なスナップショットのズレを許容する」という運用方針にしています。 まとめ: 高スループットを支える設計要素 両モジュールでは、 「テーブルの物理的なデータ配置に沿って分割し、その分割単位を分散ワーカーで並列に読み取る」  という共通する設計思想で実装しました。 以下  SpannerSource (SpannerのPartitionQuery等を活用したモジュール)も加えた各設計要素の比較表です。すでにいずれかのDBに親しんでいる人は別のDBの機能と比較することで関心・理解が深まるかもしれません。 要素 SpannerSource TiDBSource PostgresSource 物理分割の基準 Split境界( PartitionQuery ) TiKV Region境界( TABLESAMPLE REGIONS() ) ctid 物理ブロック範囲( pg_relation_size ) 分割キーの自動抽出 Spannerが自動分割(指定不要) PK /  _tidb_rowid  を自動解決 ctid (全テーブル共通) 分割計画のコスト API呼び出しのみ(フルスキャン不要) 境界キーの列挙のみ(フルスキャン不要) stat呼び出しのみ(フルスキャン・ OFFSET 不要) 範囲の読み取り Partition Tokenを実行 PK値の範囲比較でインデックスをシーク ctid の範囲比較でTID range scan 転送機構 gRPCストリーミング( executeStreamingSql ) ストリーミングResultSet( fetchSize=MIN_VALUE ) バイナリCOPY( COPY ... TO STDOUT BINARY ) 一貫性 BatchReadOnlyTransaction ( TimestampBound ) tidb_snapshot によるMVCCスナップショット バッチ読み取り( ctid スナップショットのズレは許容) 並列化 Partition Tokenを Reshuffle  +  ParDo で分散 Reshuffle  +  ParDo (dumplingのWriter並列に相当) Reshuffle  +  ParDo バージョン保護 Partition Tokenで自動保持 tidb_gc_life_time  の引き上げで代替 (該当なし) フォールバック Partition Query → 通常Query Region → 数値MIN/MAX → 全体 TID range scan → シーケンシャルスキャン(PG14未満) 汎用JDBCに対する優位性は、(1) 分割キーを自動抽出でき、分割計画も安価なので並列度を素直に上げられること、(2) 各ワーカーの転送がネイティブ機構でCPU/メモリ効率が高いこと、(3) DB固有のスナップショット機構で一貫性を担保できること(TiDB) の3点に整理できます。これらはもともとSpannerSourceがSpannerの機能で実現していた特性を、今回 TiDB / PostgreSQL 固有の機能を組み合わせて再現したものになります。 おわりに 多数のマイクロサービスが抱える大量のテーブルをDWHへ同期するという要件に対し、汎用的なJDBC経路ではなく、TiDBとPostgreSQL(AlloyDB)それぞれの  分散ストレージアーキテクチャや物理データ配置を活かした専用Sourceモジュール  をCloud Dataflow上に実装しました。これは、すでにSpannerからの取得で高スループットを実現できていた「分割 → 配布 → 並列スナップショット読み取り」というパターンを、他のDBにも横展開した取り組みと言えます。 今回は各DBが公式ツール( dumpling  / pg_dump )で利用されている高速化のノウハウを参考にさせてもらい、Dataflowの分散実行モデルに取り込みました。「分割キーの自動抽出」「物理配置に沿った安価な分割」「ネイティブなバルク転送」「DB固有のスナップショット」という要素の組み合わせが、大規模テーブルでも高いスループットと一貫性を両立させる助けになりました。 次の記事は cyan さんの「Scaling Myself: How I Run 22 Claude Code Sessions for DS4 Migration」です。引き続きお楽しみください。

動画

書籍