Runtime Lifecycle and Session Boundaries

Runtime được boot, giữ sống và share như thế nào; đâu là boundary riêng của từng notebook session.

From Architecture to a Running Runtime

Ở Runtime Architecture from Process Signals, chúng ta đã điểm qua các process chạy trên driver node như Chauffeur, DriverDaemon và Spark Master, cùng các process executor trên worker. Tuy nhiên, biết những process này tồn tại vẫn chưa đủ để hiểu runtime thực sự vận hành như thế nào. Điều cần làm rõ tiếp theo là mối quan hệ giữa notebook, execution context và cluster: Spark session được khởi tạo và gắn với notebook ra sao, một cell gặp lỗi ảnh hưởng đến runtime đến mức nào, và nhiều notebook có thể sử dụng chung compute mà vẫn duy trì các session riêng biệt như thế nào.

Nhìn từ UI, all-purpose compute có vẻ như một Spark environment luôn sẵn sàng để notebook sử dụng. Nhưng khác với mô hình Spark application thông thường, nơi một lần spark-submit khởi chạy driver, tạo SparkContext, thực thi workload rồi kết thúc, all-purpose compute được thiết kế để duy trì driver runtime qua nhiều notebook và nhiều lần thực thi command.

Điều này tạo ra hai yêu cầu song song: driver application phải tồn tại độc lập với vòng đời của từng notebook, trong khi execution state của các notebook vẫn cần được quản lý theo những ranh giới riêng. Platform cũng phải theo dõi trạng thái của runtime — từ quá trình khởi động, thời điểm sẵn sàng nhận workload, cho đến khi driver không còn phản hồi hoặc compute cần mở rộng tài nguyên worker.

Để hiểu cơ chế này, tớ nghĩ mình có thể tạm chia runtime thành ba lớp vòng đời phân biệt:

Compute / driver runtime
  └── Spark application + JVM driver
        └── Notebook session / REPL / language namespace
              └── Cell command

Mỗi lớp có vòng đời và phạm vi ảnh hưởng riêng. Một cell thất bại không nhất thiết làm notebook session hoặc compute dừng lại. Tương tự, khi notebook detach, Spark application vẫn có thể tiếp tục chạy trên compute. Ngược lại, nếu driver runtime kết thúc, các session phụ thuộc vào driver đó sẽ mất môi trường thực thi và không thể tiếp tục sử dụng runtime hiện tại.

Tuy nhiên vì mục tiêu giữ mọi thứ ngắn gọn, trong tài liệu này, tớ chỉ tập trung vào lifecycle của compute và driver runtime, từ thời điểm Databricks bắt đầu bootstrap môi trường thực thi, khởi chạy các service trên driver, khởi tạo Spark application cho đến khi runtime sẵn sàng phục vụ workload. Sau khi đi vào trạng thái hoạt động, runtime còn phải tự theo dõi health, nhận diện các trạng thái lỗi và cung cấp thông tin về workload để platform có thể đưa ra quyết định về tài nguyên.

Where Observation Begins

Một all-purpose cluster bắt đầu từ thao tác Start, hoặc từ việc attach notebook vào cluster chưa chạy. Trước khi Databricks Runtime xuất hiện, control plane còn phải chọn instance, provision VM/network/disk, lấy capacity từ cloud provider và đưa runtime environment lên đó. Đây là phần rất quan trọng của autoscaling, nhưng snapshot đang đọc là từ bên trong một cluster đã chạy; nó không cho thấy control plane đã gọi API nào hay chọn VM ra sao.

Điểm quan sát bắt đầu muộn hơn. Lúc này environment đã có mặt: mount, local disk, config và biến môi trường đã được chuẩn bị, rồi DBR bắt đầu chạy các script để dựng service. Nói cách khác, mình chưa nhìn được quá trình có compute, nhưng nhìn được khá rõ quá trình biến compute đó thành một interactive runtime.

control plane / cloud provider
  → VM + network + disk + runtime environment
  → DBR bootstrap trên driver node
  → long-lived driver runtime
  → notebook có thể attach

Giới hạn này cũng giúp tránh một cách đọc quá tay: thấy script trong runtime image không có nghĩa script đó là toàn bộ control plane. Nó chỉ là đoạn tiếp theo, sau khi platform đã giao môi trường thực thi cho DBR.

Booting a Long-Lived Driver Runtime

Artifact rõ nhất ở đây là spark/scripts/nimbus/dbr_entry_point.py. Nó làm pre-start work, chuẩn bị filesystem/local disk, chạy init script khi được bật, rồi rẽ sang driver hoặc executor path. Ở driver path, bootstrap mở hai service nền:

DBR bootstrap dispatcher
  ├─ start_chauffeur.sh
  │    └─ JVM: ChauffeurDaemon
  └─ start_spark_master.sh
       └─ JVM: Spark Master

ChauffeurDaemon
  └─ start_driver.sh
       └─ JVM: DriverDaemon
            ├─ DriverCorral
            ├─ comm/routing endpoints
            └─ SharedDriverContext / SparkContext

Spark Master và Chauffeur được khởi động gần nhau, nhưng chúng giải quyết hai việc khác nhau.

  • Spark Master là phần Spark deployment substrate: worker đăng ký vào đó, executor được launch trên worker, và Spark driver nói chuyện với nó để dùng compute đã có. Nó giúp Spark application có worker/executor để chạy task.
  • Chauffeur không phải Spark scheduler. Nó đứng ở rìa driver runtime: nhận control request, khởi động DriverDaemon, theo dõi driver, và làm gateway để platform hỏi trạng thái runtime.

Điểm này quan trọng vì một all-purpose cluster không chỉ có “Spark Master + executors”. Nó còn có một lớp giữ cho interactive driver application sống qua nhiều notebook. Không có lớp đó, mỗi notebook phải tự dựng và tự hủy driver như một application độc lập; trải nghiệm attach nhanh với spark có sẵn sẽ không tồn tại theo cách này.

Initialization Is More Than Starting Three JVMs

Đoạn diagram trên mới là skeleton process. Phần init thật dài hơn nhiều. Startup event trong driver artifact tách gần một trăm phase, và chúng gợi ra rằng DBR không chỉ start Java rồi chờ notebook gọi Spark.

Trước hoặc song song với lúc DriverDaemon hoàn tất startup, runtime còn chuẩn bị những thứ như filesystem permission và container security, network/iptables rule, Python root environment, library directory, pip index, private endpoint, Web Terminal/Git agent, rồi preload class và JDBC driver. Những bước này nghe có vẻ phụ, nhưng chúng giải thích vì sao một interactive cluster phải có lifecycle riêng: notebook không chỉ chạy một JVM Spark, mà còn cần Python environment, library path, filesystem boundary, credential/network path và tool surface hoạt động cùng nhau.

Sau đó mới tới phần driver runtime: tạo DriverDaemon, configure DriverCorral, mở routing server và communication channel, rồi gửi DriverIsReady về Chauffeur. Từ đây có thể tách ba ý dễ bị gộp:

process boot
  → các service/JVM có mặt

runtime initialization
  → environment, config, endpoint, Corral và language surface được chuẩn bị

interactive readiness
  → Spark context, REPL và dependency cần cho attach đã usable

Config cũng không chỉ là một file Spark conf được đọc lúc đầu. Artifact SAFER cho thấy DBR phân biệt ít nhất config theo process, theo allocation của cluster, và theo session. Điều này hợp với runtime dài hạn: có config được chốt khi JVM start, có config chỉ biết sau khi driver được gán vào một cluster cụ thể, và có config theo execution context/notebook. Một số config còn bị pinned để user code không ghi đè tuỳ ý. Mình chưa cần đào hết cơ chế này ở page lifecycle, nhưng nó là một lý do khác khiến lifecycle interactive phức tạp hơn một spark-submit.

Cuối cùng còn có các nhánh tối ưu latency: preloading class/JDBC driver, warmup interpreter/query, pre-initialized REPL, thậm chí warm driver chờ được gán. Không phải nhánh nào cũng bật trong snapshot hiện tại, nhưng chúng cho thấy “init” ở đây là bài toán cân bằng giữa hai lựa chọn: tạo mọi thứ từ đầu khi user attach, hoặc giữ sẵn một phần runtime để đổi lấy latency thấp hơn.

Chauffeur Starts and Watches the Driver

Sau bootstrap, Chauffeur gọi start_driver.sh để spawn DriverDaemon. Script chuẩn bị environment/classpath rồi chạy JVM com.databricks.backend.daemon.driver.DriverDaemon; PID và exit code được giữ lại để Chauffeur biết driver đã dừng thế nào.

Nói ngắn gọn, DriverDaemon là nơi Spark driver thật sự sống. Trong JVM này có SparkContext, scheduler stack của Spark, SharedDriverContext, DriverCorral và các REPL notebook. SparkContext không phải process riêng để notebook tự tạo rồi connect tới. Nó là object nằm trong heap của DriverDaemon JVM, được DatabricksILoop tạo hoặc lấy lại qua SharedDriverContext.

Chauffeur ở ngoài JVM đó nên có thể quan sát driver như một service mà nó quản lý. DriverDaemonMonitor có các trạng thái như Starting, Running, Unresponsive, Stopping và Stopped. Khi driver dừng, Chauffeur còn có thể phân biệt startup error, SparkContext stopped, driver không phản hồi hay một stop có chủ đích, trước khi platform xử lý phần tiếp theo.

Đây là ranh giới đầu tiên làm Databricks khác cảm giác Spark application thuần: driver không chỉ là process do user code vô tình tạo ra. Nó được runtime coi là một service dài hạn có lifecycle riêng.

A Driver Process Can Exist Before SparkContext Does

Một nuance dễ bỏ qua: DriverDaemon đã chạy không có nghĩa SparkContext chắc chắn đã được tạo ngay lúc đó. Trong configuration mặc định của snapshot, DriverCorral có thể chỉ dựng SharedDriverContext khi command/REPL đầu tiên thực sự cần nó. Nói cách khác, cluster có thể đã có DriverDaemon, routing endpoint và lifecycle monitor, nhưng chưa trả giá tạo full Spark driver cho đến lúc interactive work chạm vào nó.

DriverDaemon starts
  → Corral/routing surface becomes available
  → first REPL or command needs a shared environment
  → DatabricksILoop gets or creates SharedDriverContext
  → SharedDriverContext creates/holds SparkContext

Databricks cũng có một nhánh warmup: runtime có thể tạo trước SharedDriverContext và warm interpreter để giảm latency của lần attach/command đầu tiên. Nhưng đây là optimisation có điều kiện, không phải thứ bắt buộc trong mọi cluster. Với snapshot hiện tại, cờ warmup driver không được set; đường lazy mới là đường phù hợp hơn để hình dung.

Điều này làm rõ hơn tại sao phải tách driver process với interactive Spark context. Một process đã launch là điều kiện cần để nhận control request. Còn SparkContext đã sống, REPL đã sẵn và dependency đã ready mới là điều kiện gần hơn với trải nghiệm “mở notebook là dùng được spark”.

Ready Means More Than a Live Process

Một JVM vừa start chưa chắc đã dùng notebook được. Nó có thể đang dựng Spark context, đang kẹt ở dependency nào đó, hoặc process còn sống nhưng không còn phản hồi. Vì vậy Chauffeur không chỉ spawn DriverDaemon rồi đứng ngoài. Nó là health gateway đứng bên ngoài driver JVM.

Control path có thể gọi GetChauffeurState qua endpoint local của Chauffeur để hỏi runtime đang ở trạng thái nào, thay vì đi thẳng vào JVM đang chạy Spark và notebook code. Health response có SparkContext ID, sparkError, driver responsiveness, và driverComponentHealth cho các dependency như metastore và DBFS.

control path
  → hỏi Chauffeur
  → Chauffeur hỏi/đọc driver state
  → trả driver responsive hay không
  → kèm readiness của runtime dependencies

Đây là lý do health không thể hiểu đơn giản là “DriverDaemon còn PID hay không”. Nếu SparkContext chưa lên hoặc Spark gặp error, runtime có thể được xem là Spark down. Nếu driver không responsive, runtime chưa sẵn sàng dù process vẫn còn đó. Nếu một dependency readiness check fail, attach có thể không nên được coi là an toàn chỉ vì Java process còn chạy.

Từ snapshot, mình thấy metastore và dbfs xuất hiện trong component-health map. Bên driver có MetastoreMonitor và DBFS readiness client, còn Chauffeur trả trạng thái tổng hợp ra health RPC. Cách đọc hợp lý là interactive runtime không chỉ cần Spark driver chạy; nó còn cần một số dependency nền có thể dùng được ở mức tối thiểu.

Tuy nhiên, không nên đọc quá xa hơn evidence. Client/monitor trong driver runtime không chứng minh metastore hay DBFS là service dedicated cho riêng cluster. Chúng có thể external/shared, đi qua proxy, hoặc có sidecar trên node. Snapshot cũng chưa đủ để nói một readiness failure nhất định sẽ chỉ block attach, restart driver hay gây recovery theo policy nào.

Điểm sâu hơn ở đây là runtime được vận hành như một khả năng phục vụ notebook, không phải một tập PID còn sống. Driver phải dựng xong Spark context, control channel phải còn thông, và dependency nền phải đủ ready. Chauffeur tạo một điểm quan sát độc lập ở ngoài driver — hữu ích khi driver bận, kẹt GC, hoặc đang ở giữa một command lỗi.

Attaching to a Shared Driver

Khi cluster đã ready, notebook mới attach vào runtime ấy. Trực giác “attach là connect thẳng vào một ILoop đang giữ driver” gần đúng ở chỗ notebook cuối cùng cần interpreter/REPL để chạy code, nhưng sai ở ownership.

ILoop không giữ Spark driver riêng. Ngược lại, DriverDaemon cũng không chỉ là vỏ mỏng giữ context. DriverDaemon JVM giữ Spark driver, còn ILoop/REPL là lớp diễn giải code notebook theo language ở phía trên driver đó.

DatabricksILoop là path tạo hoặc lấy lại SharedDriverContext; chính đường này tạo SparkContext và đặt nó vào context dùng chung. Tuỳ warmup mode, Spark context có thể được dựng trước attach để rút ngắn thời gian chờ, hoặc được DriverCorral kéo lên lazy khi lần đầu cần. Dù theo đường nào, Spark driver vẫn nằm trong heap của DriverDaemon JVM.

Khi notebook attach, flow quan sát được là:

notebook attach
  → Chauffeur tạo SessionId / ExecutionContextId
  → StartRepl(ReplId, language, execution context)
  → DriverDaemon / DriverCorral
  → REPL / interpreter theo ngôn ngữ
  → SharedDriverContext → SparkContext

Chauffeur forward control request; DriverCorral bên trong DriverDaemon mới tạo và quản REPL tương ứng. Với Scala, đó có thể là nhánh DriverILoop; Python đi qua PythonDriverLocal và Jupyter-kernel path. Tên DriverLocal hơi dễ gây hiểu lầm: nó không phải một Spark driver khác. Nó là lớp local/interpreter phục vụ một REPL, còn SparkContext vẫn được mượn từ SharedDriverContext bên dưới.

Vì vậy hai notebook có thể vừa chung vừa riêng cùng lúc:

  • Chúng chung Spark application và phần scheduler-level state nằm trong DriverDaemon.
  • Nhưng mỗi attach vẫn có SessionId, ExecutionContextId, ReplId, thread, classloader và language state riêng.

Đây là phần giải thích cho cảm giác spark có sẵn nhưng không có nghĩa mọi Python variable, Scala val/var hay interpreter state đều trở thành global. Chi tiết một command đi từ notebook, qua Chauffeur/DriverCorral, vào REPL rồi quay output về client sẽ được lần kỹ hơn ở Command Execution Path.

Reporting Pressure Back to the Platform

Sau khi runtime phục vụ notebook, câu hỏi scale không còn chỉ là “Spark có thiếu executor không?”. Một all-purpose cluster phải nhìn ít nhất ba kiểu pressure khác nhau:

  1. Spark có task đang đợi slot trên executor không?
  2. Người dùng có đang dồn command/notebook activity vào cùng runtime không?
  3. Chính driver có đang trở thành điểm nghẽn trước cả executor không?

Đó là lý do có bề mặt GetAutoscalingInfo: Chauffeur lấy runtime/load view từ DriverCorral rồi trả AutoscalingInfo ra control path. Schema cho thấy ít nhất ba nhóm signal:

  • schedulerStats.pendingTasks và numTaskSlotsTotal: backlog Spark so với slot task hiện có.
  • peakRunningCommandsInfo, peakRunningSyncCommandsInfo, peakRunningVeryShortQueriesInfo, peakRunningMetadataRpcsInfo: pressure quanh interactive command, không thuần là Spark task.
  • clusterThroughputInfo và driverLoad: throughput/load của runtime driver.

Nói cách khác, Databricks không chỉ nhìn một Spark scheduler đang thiếu executor. Nó cố nhìn workload theo cách phù hợp hơn với interactive compute: có người đang chờ cell chạy không, driver có đang nghẽn không, và cluster hiện có còn đủ capacity không.

Why This Is Not Spark Dynamic Allocation

Đây không chỉ là Spark Dynamic Allocation ở một level khác. Apache Spark Dynamic Allocation để driver tự tăng/giảm executor theo workload. Databricks autoscaling quản worker node và executor lifecycle ở platform level.

Databricks ghi rõ không được bật spark.dynamicAllocation.enabled trên compute dùng Databricks autoscaling. Hai loop cùng cố điều khiển executor/worker lifecycle có thể đưa ra scaling decision mâu thuẫn, gây executor churn, NODES_LOST, hoặc task không được pick up.

Đường signal nhìn từ runtime hiện tại có thể tóm tắt như sau:

notebook commands + Spark scheduler
  → DriverCorral gom runtime/load view
  → Chauffeur trả AutoscalingInfo
  → control plane quyết định capacity ở ngoài cluster
  → worker mới vào / worker cũ được decommission

Từ snapshot còn thấy đường scale-down quay ngược vào runtime: control path có thể đi qua Chauffeur/DriverCorral để SparkContext decommission worker êm trước khi platform thu hạ tầng. Đoạn này hợp lý hơn việc rút worker ngay giữa workload, vì Spark driver còn cần được báo để chuyển/đóng executor đúng cách.

What the Spark Master Still Does

Vậy khi platform đã lo chuyện tăng/giảm số worker node, Spark Master còn làm gì? Vai trò của nó hẹp hơn chuyện quyết định capacity, nhưng vẫn là phần Spark không thể bỏ.

Khi control plane thêm một worker/VM, worker runtime khởi động rồi tự đăng ký vào Spark Master. Master giữ danh sách worker nào đang có mặt và worker nào còn tài nguyên để phục vụ Spark application. Phía driver, StandaloneSchedulerBackend nói chuyện với Master để xin executor; Master nhận request đó và launch executor của application lên worker phù hợp. Sau khi executor lên, driver mới gửi task xuống executor qua scheduler của Spark.

control plane thêm worker node
  → worker runtime đăng ký vào Spark Master
  → Driver/Spark scheduler xin executor
  → Master launch executor trên worker đã đăng ký
  → driver gửi task tới executor

Vì vậy có thể tách rõ ba việc thường bị gộp thành một chữ “scale”:

  • Platform autoscaling quyết định cần thêm hay bớt worker node/VM dựa trên signal từ runtime.
  • Spark Master giữ registry worker và thực hiện phần Spark deployment: nhận worker mới, gán/launch executor cho application, rồi xử lý worker/executor bị rút.
  • Spark driver scheduler biến Spark job thành task và phân task xuống các executor đã có.

Nói chính xác hơn, Master không chỉ “đăng ký worker rồi bổ sung executor” một cách tự động, và nó cũng không tự quyết worker nào cần được cloud provider tạo ra. Nó là lớp trung gian để worker mới trở thành usable resource của Spark application, rồi để driver đặt executor/task lên resource đó. Với Databricks all-purpose compute hiện tại, autoscaling decision được đặt ở control-plane loop bên ngoài Master.

Three Different Resource Decisions

Chữ “scale” che mất ba quyết định khác nhau, và đây là chỗ mình từng dễ gộp Spark với hạ tầng thành một cơ chế:

1. node capacity
   control plane: có cần một VM/worker mới không?

2. executor placement
   Spark driver + Master: worker đã có thì application đặt executor ở đâu?

3. task scheduling
   Spark driver: executor đang có thì task nào được chạy trước?

Ở tầng đầu, Databricks cần nhìn pressure của toàn interactive runtime rồi đi ra cloud provider. Ở tầng hai, Spark Master không biết notebook nào đang đợi cell hay cloud có bao nhiêu capacity; nó chỉ biết worker nào đã vào cluster và còn có thể phục vụ application. Ở tầng ba, DAGScheduler/TaskScheduler trong DriverDaemon mới chia một Spark job thành stage/task rồi gửi task xuống executor.

Phân biệt này cũng giải thích tại sao thêm worker không đồng nghĩa query ngay lập tức nhanh hơn. Worker mới phải được platform provision, worker runtime phải khởi động và register vào Master, driver phải có executor trên worker đó, rồi scheduler mới có slot để đặt task. Ngược lại, khi scale-down, platform không nên xem worker như một VM có thể rút đi vô điều kiện; Spark driver còn phải biết executor nào đang ở đó để decommission/di chuyển work theo cách ít phá vỡ application hơn.

Nhưng AutoscalingInfo mới cho thấy bề mặt tín hiệu, chưa cho thấy thuật toán. Nó không nói được trọng số, threshold, cadence hay policy thật sự quyết định provision/tháo VM. Vậy nên có thể nói Databricks có một control loop đi từ runtime ra platform, nhưng chưa thể nói chính xác loop đó ra quyết định thế nào từ snapshot này.

My Summary

Thứ mình thấy hay ở đây là Databricks đã làm một chuyện mà trước đó mình luôn tò mò nhưng khó tự trả lời bằng Spark thuần: làm sao để một Spark application sống đủ lâu, đủ ổn định và đủ quan sát được để nhiều notebook có thể bước vào, rời đi, rồi lại tiếp tục dùng chung nó như một interactive service.

Nhìn từ ngoài thì chỉ là mở cluster, attach notebook và có sẵn spark. Nhưng để trải nghiệm đó tồn tại, runtime phải bọc driver bằng một lớp lifecycle/control riêng: có process supervisor, readiness không chỉ nhìn PID, session/REPL boundary cho từng notebook, và một đường báo pressure trở lại platform để thay đổi capacity. Cảm giác có hơi “hacky” theo nghĩa đây không còn là đường chạy thẳng của một Spark application nữa; nó là một lớp runtime khá đồ sộ nằm quanh Spark, với nhiều điểm có thể fail và nhiều trạng thái phải giữ nhất quán.

Có lẽ đây cũng là một lý do hợp lý để phân biệt all-purpose compute với job compute. All-purpose compute tối ưu cho vòng lặp khám phá dài hạn, nơi interactive state và thời gian attach quan trọng. Nhưng production workload thường dễ vận hành hơn khi mỗi job có lifecycle rõ hơn, ít state sống lâu hơn và ít bề mặt session/control cần giữ hơn. Đây là cách mình đọc kiến trúc hiện tại, không phải kết luận rằng all-purpose compute không thể chạy production workload.

On this page