Runtime Architecture from Process Signals

Dựng overview architecture của Databricks interactive runtime từ port, PID, process tree và các process thật trong driver container.

From Spark Application to Interactive Runtime

Như tớ đã trình bày ở phần Databricks Interactive Runtime, trong Spark truyền thống, driver thường sống cùng vòng đời của một application. Databricks interactive runtime lại tổ chức ranh giới này theo cách khác: notebook đóng vai trò client gửi các command tương tác, còn driver-side Spark runtime được duy trì lâu dài và được platform quản lý như một service.

Tuy nhiên, notebook không phải lúc nào cũng giao tiếp với Spark runtime qua cùng một boundary. Trên Standard hoặc Serverless, tùy theo language và Databricks Runtime version, notebook có thể sử dụng Spark Connect để gửi execution plan tới Spark runtime phía sau. Trong khi đó, Dedicated vẫn giữ execution path gần với Spark classic hơn: notebook command đi qua cơ chế session/REPL nội bộ của Databricks và được thực thi trên một long-lived Driver runtime, nơi nhiều notebook có thể dùng chung Spark application và SparkContext nhưng vẫn giữ SparkSession riêng.

Một giả thuyết cần tiếp tục kiểm chứng là Databricks duy trì cluster bằng một long-running Spark application, có thể mang tên Databricks Shell, rồi đặt lên trên nó một lớp interactive runtime để tiếp nhận, điều phối và cô lập các notebook session. Như vậy, bài toán không chỉ là giữ SparkContext không bị stop, mà là quản lý toàn bộ lifecycle của runtime: nó được boot qua những phase nào, khi nào được xem là ready, process nào nhận và điều phối command, session state được cô lập ở đâu, cũng như platform phát hiện trạng thái idle, busy hoặc broken bằng cách nào.

Runtime này còn phải cung cấp các health và load signal cho platform quản lý capacity. Spark dynamic allocation chỉ phản ứng với executor demand bên trong Spark application; tự nó không thể giải thích toàn bộ lifecycle, session load và autoscaling behavior của một interactive compute.

Vậy Databricks xây dựng và quản lý long-lived interactive runtime này như thế nào, đồng thời hỗ trợ cả Spark Connect boundary trên các runtime mới lẫn execution path nội bộ gần Spark classic trên Dedicated compute?

Disclaimer

Đây là ghi chú cá nhân dựa trên các quan sát trong runtime, tài liệu/paper public và artifact có thể đọc được từ tài liệu công khai và môi trường được cho phép. Nội dung là phỏng đoán kỹ thuật, không phải tài liệu chính thức của Databricks và có thể sai hoặc thiếu so với production runtime.

Runtime Architecture from Process Signals

Ta có thể bắt đầu bằng một phép quan sát khá cơ bản: kiểm tra những process nào đang lắng nghe trên các network port bên trong runtime. Kết quả từ netstat hoặc ss cho thấy ngoài các endpoint quen thuộc của Spark, Driver Node còn chạy nhiều service riêng của Databricks:

PortThành phầnVai trò quan sát được
7077Spark Master RPCKênh RPC của Spark Standalone bên trong cluster
40000Spark Master UIGiao diện quản trị của Spark Master
40001Spark application UIGiao diện của Spark application đang chạy trên Driver
6060Chauffeur HTTPS APIHealth và control surface cục bộ của ChauffeurDaemon
7788 / 7789Databricks info serviceExpose một số thông tin nội bộ của runtime
47247Spark Worker Web UIGiao diện của Spark Worker trên Worker Container
4048External Shuffle ServicePhục vụ shuffle data phía Worker
7100SafeSpark daemon RPCKênh RPC tới sandbox dùng để thực thi UDF
8000SafeSpark UDF serverServer thực thi UDF bên trong sandbox
5657IAM token readinessEndpoint host-side báo trạng thái sẵn sàng của token service
9207Chưa xác địnhCó dấu vết liên quan tới metastore và cấu hình RDS_DIRECT; chưa xác định đây là legacy Hive Metastore, Unity Catalog path hay một internal metadata services
9210Storage proxyCó dấu vết liên quan tới storage/Hadoop traffic; chưa xác định service đứng sau hoặc vai trò của nó trong Unity Catalog

Bảng này mới chỉ cho biết những endpoint nào đang tồn tại; nó chưa giải thích các thành phần đứng sau chúng liên hệ với nhau như thế nào. Để đi tiếp, ta có thể lần từ port về PID, rồi dùng ps, jps/proc để đọc command line, JVM main class cũng như quan hệ parent–child giữa các process. Từ đó, Driver Container hiện ra không phải như một Spark Driver JVM đơn lẻ, mà là một runtime gồm nhiều process và component phối hợp với nhau:

dbr_entry_point.py
  ├─ ChauffeurDaemon          health/control API :6060
  │    └─ DriverDaemon        Spark application UI :40001
  │         └─ DriverCorral   command/REPL/session dispatcher
  └─ Spark Master             RPC :7077 · UI :40000

Sơ đồ này cần được đọc theo hai mức:

  • ChauffeurDaemon, DriverDaemonSpark Master là những process hoặc runtime boundary có thể lần ra từ hệ điều hành.
  • DriverCorral là một component nằm bên trong DriverDaemon, chịu trách nhiệm điều phối command, REPL/session, status và load information; nó không nhất thiết là một JVM process độc lập.

Bên ngoài Driver Container còn có Spark Worker và Executor chạy trên các Worker Container, SafeSpark daemon cùng UDF server trong môi trường sandbox, cũng như một số endpoint kết nối tới hạ tầng host-side hoặc Control Plane. Những dấu vết này cho phép ta dựng nên một bản đồ sơ bộ: thành phần nào thực sự tồn tại dưới dạng process, thành phần nào expose network endpoint, phần nào thuộc Spark execution substrate và request từ notebook hoặc Control Plane có khả năng đi vào Data Plane qua những đường nào.

Runtime architecture reconstructed from process, network, and trust-boundary signals

Why Databricks Need this Architecture?

Trước khi đi vào tìm hiểu chi tiết về các components, tớ nghĩ mình cần đi ngược lại một chút. Đó là động lực nào mà Databricks phải chọn thiết kế này, hay nói đúng hơn là thực sự những bài toán, vấn đề mà thiết kế này giải quyết là gì. Câu trả lời cuối cùng đơn giản hơn tớ nghĩ, nó gói gọn trong một câu:

Spark open-source được thiết kế cho một người dùng đáng tin, chạy một application; Databricks phải cho hàng nghìn người xa lạ dùng chung một hạ tầng, đồng thời.

Gần như mọi thứ được bổ sung trong DBR đều là hệ quả của đúng một khác biệt đó. Không phải Databricks thích phức tạp, mà mỗi lớp phức tạp được thêm vào để giải một bài toán rất cụ thể mà Spark OSS không cần giải.

Why Does Spark OSS Assume a Trusted User?

Khi ta viết một Spark Application rồi chạy, chuyện xảy ra là:

Tùy vào deploy mode, một JVM sẽ được khởi động để chạy Driver.

  • Với cluster mode, Cluster Manager chịu trách nhiệm tạo Driver process trên cluster: YARN chạy Driver trong ApplicationMaster container, còn Kubernetes chạy nó trong một Driver Pod.
  • Với client mode, Driver JVM chạy ở phía client, chẳng hạn trong process local của spark-submit hoặc spark-shell, hay gắn với kernel nếu chạy notebook.

JVM đó sẽ tạo ra một SparkContext, SparkContext liên lạc với cluster manager để xin cấp executor và cluster manager bảo các Worker fork ra process executor.

  • Trong standalone mode — cũng là mode mà Databricks dùng bên dưới — đó là Spark Master.
  • Với YARN vai trò này thuộc về ResourceManager.
  • Và với K8S thì nó là Scheduler.

Executor kết nối về driver, nhận task, chạy, trả kết quả.

Khi job xong hoặc code chủ động được stop, driver JVM sẽ thoát và hết.

Bây giờ ta cần để ý vài điều mà có lẽ trước giờ chưa từng phải nghĩ tới, vì chúng gần như hiển nhiên.

Driver JVM là application của mình. Không có ai khác trong đó. Code ta viết và code của Spark nằm trong cùng một trust boundary, có thể dùng chung heap, classpath và OS user. Muốn gọi System.exit() thì cả application chết; muốn đọc sparkContext.getConf để xem có gì thì cứ đọc — kể cả những key nhạy cảm nếu chúng được đặt trực tiếp trong Spark configuration. Chẳng sao cả, vì tất cả đều thuộc về application của mình. Không có lớp sandbox nào đứng giữa user code và phần “hạ tầng” của runtime, đơn giản vì mô hình này không cần nó.

Credential cũng là chuyện của môi trường. Ta có thể export AWS_SECRET_KEY hoặc gán một IAM role cho instance. Spark đọc credential từ environment, configuration hoặc metadata service. Nó không cần biết “ai” đang chạy từng command, vì chỉ có một “ai”: application identity. Executors và UDF thường cũng thực thi dưới identity và tập credential của application, nên việc truy cập storage mang danh tính của application chứ không phải của người vừa gửi command.

Spark có các cơ chế security, nhưng chúng không mặc định tạo ra user-level isolation bên trong một application. spark.authenticate mặc định tắt; RPC encryption và ACLs như spark.acls.enable, spark.ui.view.acls hay spark.modify.acls cũng không tự nhiên được bật. Ai chạy được code trong Driver thì thường có thể làm mọi thứ mà application identity được phép làm. Lớp bảo mật mạnh hơn thường nằm ở bên ngoài: ai được vào máy, application được gán role nào, hoặc một Cluster Manager như YARN kết hợp với Kerberos chịu trách nhiệm authentication.

Driver vẫn là failure boundary của application. Trong client mode, Driver chết thì application kết thúc. Với một số cluster manager hoặc lớp orchestration bên ngoài, chẳng hạn YARN trong cluster mode, Driver/application có thể được launch lại dưới dạng một application attempt mới. Nhưng đó không phải là khôi phục nguyên Driver JVM cũ: SparkContext, REPL state, cached state phía Driver và các object trong memory vẫn mất.

Ngay cả các multi-user gateway của OSS cũng chủ yếu vá ở bên ngoài chứ không thay đổi giả định cốt lõi này. Spark Thrift Server hay Livy thường thực thi query dưới identity của server; muốn chạy dưới danh tính của người gửi thì phải dựa vào những cơ chế như impersonation hoặc Hadoop doAs hay proxyUser. Việc identity phải được “mượn” từ một tầng khác cho thấy Spark không tự mang khái niệm multi-tenant user bên trong execution model của nó.

Tổng hợp lại, ta có đúng mô hình mà Spark được thiết kế cho: một người dùng đáng tin chạy một application trong một cluster đáng tin. Trust boundary tự nhiên của Spark nằm ở cấp application, không phải cấp user — ai chạy được code trong application thì mặc nhiên có toàn bộ quyền của application đó.

Đó là một thiết kế rất hợp lý cho bài toán mà Spark nhắm tới. Giờ ta đổi bài toán.

The First Requirement: Multiple Users Sharing a Cluster

Databricks cung cấp Spark as a Servcie. Người dùng mở web, viết code trong notebook rồi bấm chạy.

Cách ngây thơ nhất là mỗi lần có người bấm chạy, ta spark-submit một application mới. Nhưng notebook không hoạt động như thế. Người ta chạy cell này, xem kết quả, rồi chạy cell tiếp theo; biến và state từ cell trước vẫn phải còn. Đó là một REPL, không phải một job độc lập.

REPL - Read-Eval-Print Loop

Read-Eval-Print Loop - REPL là vòng lặp read - eval - print: đọc một đoạn code người dùng vừa nhập, evaluate nó, in kết quả ra, rồi quay lại chờ đoạn tiếp theo. Điểm quan trọng không nằm ở vòng lặp, mà ở chỗ state được giữ lại giữa các lần evaluate: biến, function, class, import và cả session config do lần trước tạo ra vẫn còn sống để lần sau dùng tới.

Khác biệt với một batch job nằm ở đơn vị công việc mà runtime nhận được. Một batch job nhận trọn program ngay từ đầu, chạy từ trên xuống rồi kết thúc; runtime biết trước toàn bộ những gì sẽ xảy ra. Một REPL thì không: nó chỉ biết đoạn code vừa được gửi tới, và không biết người dùng sẽ gửi gì tiếp theo — hoặc có gửi nữa hay không.

Điều đó đặt ra vài yêu cầu mà một job không cần:

  • Phải có một process sống lâu hơn từng lần evaluate, vì nó là chỗ giữ state; process chết là mất hết những gì đã build ra.
  • Phải giữ được binding và class do chính user định nghĩa: df khai báo ở cell trước phải còn ở cell sau, và một case class định nghĩa lúc trước phải còn dùng được về sau.
  • Output phải chảy về dần trong lúc code đang chạy, chứ không chỉ xuất hiện một lần khi mọi thứ kết thúc.
  • Một lần evaluate lỗi không được giết cả session: exception chỉ nên làm fail đúng cell đó, phần state còn lại vẫn phải dùng tiếp được.

spark-shellpyspark chính là REPL theo nghĩa này — và trong Spark, REPL không chỉ là chuyện tiện tay ở phía client. Mỗi dòng code trong Scala REPL được compile thành class ngay tại driver, nên khi một closure tham chiếu tới class vừa định nghĩa, executor phải fetch được class đó từ driver để deserialize task. Đây là lý do Spark có một class server phía driver cùng các config như spark.repl.class.urispark.repl.class.outputDir: code sinh ra lúc runtime vẫn phải đi tới được executor.

Notebook cũng là một REPL, chỉ khác là mỗi cell đóng vai trò một lần evaluate, và phần print được render thành output trên UI thay vì in ra terminal.

Vậy ta phải giữ một JVM sống, có sẵn SparkContext, rồi đẩy từng cell vào đó. Được, nhưng bây giờ đến vấn đề là chi phí: một cluster gồm Driver và vài Worker mà chạy mãi chỉ để phục vụ một người dùng thì khá đắt. Nếu mười người trong cùng một team đều đang xem dữ liệu, ta muốn họ dùng chung cluster đó. Và đây là chỗ mô hình của Spark OSS vỡ.

Nếu Driver JVM là application của một user, thì mười user đồng nghĩa với mười Driver, và có thể là mười cluster. Muốn share compute, Driver không thể tiếp tục là application riêng của một người; nó phải trở thành một service phục vụ nhiều người. Code của từng user không còn tự sở hữu Driver lifecycle, mà được gửi xuống dưới dạng command để runtime tiếp nhận và thực thi. Đó là toàn bộ lý do tồn tại của DriverDaemon.

DriverDaemon không phải "driver của Spark được đổi tên". Nó là một service sống lâu, nhận request và quản lý nhiều phiên làm việc bên trong. Databricks gọi mỗi phiên đó là một execution context: một namespace thực thi riêng, giữ state và môi trường cho một notebook hoặc user session. Component quản lý các execution context này có tên là DriverCorral - “corral” nghĩa là chuồng, khá đúng với chức năng của nó.

Nếu mental model này nghe quen thì đúng là như vậy: đây cũng chính là hướng mà Spark Connect formalize. Client không còn chạy chung process và trực tiếp sở hữu Driver; nó xây execution plan rồi gửi plan qua một protocol tới Spark Server phía sau. Nhờ đó, client chết không kéo Driver chết theo, dependency phía client không làm bẩn Driver JVM, và một long-lived Spark runtime có thể phục vụ nhiều client session.

Và giờ để ý một hệ quả kéo theo: nếu Driver là service thì ai chăm nó? Trong Spark OSS, user là người theo dõi. Nếu thấy service treo thì user kill rồi chạy lại. Nhưng ở đây không có ai trực tiếp SSH vào máy để quản Driver, và nếu Driver treo thì nhiều người đang làm việc cùng lúc sẽ bị đứng. Phải có một thứ gì đó theo dõi và có khả năng restart nó. Cái đó không thể là một thread nằm trong chính Driver. Nếu Driver treo hoặc OOM thì thread ấy cũng chết theo. Nó phải là một process riêng. Đó là ChauffeurDaemon.

Và vì Chauffeur đã là một process riêng đứng trước Driver, nó tự nhiên trở thành nơi hợp lý để đặt control boundary của runtime. Đó là lý do nó có ba server — Jetty HTTPS, gRPC và một internal server để Driver gọi ngược — trong khi Driver không cần trực tiếp nói chuyện với mọi thành phần bên ngoài.

The Second Requirement: User Code Cannot Be Trusted

Trong Spark OSS, code chạy trong Driver JVM là code của một user duy nhất, nên không sao. Nhưng bây giờ Driver chứa một SparkContext dùng chung, credential của cluster, config với các endpoint nội bộ, và cả execution context của những notebook khác. Nếu cứ để user code chạy thẳng trong Driver JVM, user chỉ cần vài dòng reflection là có thể lần tới những thứ không thuộc về mình. Thứ mà đối với multi-tenant là không thể chấp nhận được. Vậy phải đẩy user code ra khỏi JVM đó. Nhưng đẩy đi đâu, và hai bên sẽ nói chuyện với nhau bằng cách nào?

Python is Easier

Với Python thì dễ hơn. Python vốn đã chạy trong một process riêng. Ngay cả PySpark trong Spark OSS cũng phải giao tiếp với JVM qua Py4J. Databricks chỉ cần đẩy boundary này xa hơn: chạy Python dưới một Linux user có quyền thấp — tên dạng spark-<uuid> — rồi cho process đó giao tiếp với Driver qua socket.

Câu hỏi tiếp theo là: dùng protocol gì?

Và ở đây Databricks chọn một cách mà tớ thấy khá hay: không tự phát minh lại từ đầu, mà dùng Jupyter protocol. Notebook process trong DBR là một ipykernel thật — cùng nền tảng kernel được dùng trong JupyterLab — nhưng được Databricks subclass qua nhiều lớp, chẳng hạn: IPythonKernel -> LspAwareKernel -> DatabricksKernel.

Ở phía còn lại, Driver JVM đóng vai trò như một Jupyter client. Nó tạo connection file chứa các trường chuẩn như shell_port, iopub_port, control_port, stdin_port, hb_port, signature_schemekey, rồi giao tiếp với kernel qua ZeroMQ với message authentication bằng HMAC.

Điều này giải thích cho một số class name trên Driver mà qua terminal có thể tìm được như ExecuteReply, CompleteRequest, InspectReplyJava hay JupyterKernelInfoReply. Bởi vì Driver đang nói Jupyter protocol, và đó chính là các message của protocol này.

Scala Is Harder

Scala REPL không giống Python. Để chạy một dòng Scala, ta cần compiler, classloader và khả năng nhìn thấy những class được tạo ra từ các cell trước. Trong Spark classic, REPL còn nằm rất gần Driver JVM: user code có thể trực tiếp sử dụng SparkContext, Spark JVM APIs và những object đã tồn tại trong runtime.

Nếu muốn tách Scala user code khỏi Driver, Databricks không chỉ cần khởi chạy thêm một process. Họ còn phải trả lời một câu khó hơn: Scala session ở JVM bên ngoài sẽ giao tiếp với Spark runtime trong Driver JVM qua boundary nào?

Từ đó, có hai hướng kiến trúc lớn:

  • Giữ Scala REPL bên trong Driver JVM: Cách này bảo toàn gần như toàn bộ Spark classic API surface vì user code, compiler và SparkContext cùng nằm trong một JVM. Ở đây, Databricks triển khai thêm một DriverILoop, một class kế thừa org.apache.spark.repl.SparkILoop (chính là spark-shell).

  • Tách Scala REPL sang một JVM riêng, rồi không cho nó truy cập trực tiếp vào SparkContext nữa. Ở đây thực sự có 2 phương án, 1 là Spark Connect, 2 là extend thêm một ScalaJupyterDriverLocal từ DriverLocal và sử dụng kernel Almond.

Chi tiết tớ sẽ trình bày rõ ràng hơn sau này. Thực sự chỗ này là cả 1 thế giới chứ không đơn giản như mô tả ở trên đâu :)).

What about UDF?

Còn khi user viết một Python UDF function df.withColumn("x", my_udf(col)) rồi gọi. Hàm đó chạy phía executor trong lúc xử lý dữ liệu, có thể trên một Worker Node đang đồng thời phục vụ workload của những user khác. Việc tách notebook process khỏi Driver vì thế vẫn chưa đủ; user code còn phải được cô lập tại nơi task thực sự chạy.

Trong runtime đang quan sát, Databricks đưa UDF vào SafeSpark sandbox. Đây không chỉ là một process hoặc container thông thường, mà còn có dấu vết của gVisor (runsc) để tạo isolation ở syscall boundary. Cả một tập thành phần như runsc, runc, crictl, CNI và các OCI image dựng sẵn cho Python, Scala hoặc JDBC xuất hiện chỉ để tạo một execution boundary an toàn hơn cho user-defined code.

The Third Requirement: Authorization Cannot Be Bypassed

Đến đây ta đã có nhiều user dùng chung cluster, và user code cũng đã được cách ly. Nhưng process isolation chưa giải quyết bài toán dữ liệu: user A vẫn không được phép đọc bảng của user B.

Với Spark SQL, chỗ hợp lý để đặt permission check là analyzer. Dù query đi vào từ spark.sql, spark.table, spark.read hay từ Python, Scala, SQL hoặc R, nó vẫn phải trở thành logical plan và đi qua analyzer để resolve table, column và type.

Trong open-source stack, những hệ thống như Ranger thường đi theo đường spark.sql.extensions: cắm thêm rule vào Spark để authorization chạy ở cùng analyzer path này, ví dụ Kyuubi Spark AuthZ Plugin. Nhưng trong snapshot Databricks này, authZ không đi qua cơ chế extension kiểu đó. Databricks cắm sâu hơn ở tầng fork của Spark, qua các seam trong SessionStateBuilder/DatabricksEdge, nơi các rule như CheckPermissionsCheckFileSystemSafety được đưa vào runtime.

Các rules này được nối trực tiếp với Unity Catalog. Ở đó, permission được gom về một catalog/control-plane chung, rồi nối quyền trên logical object với quyền truy cập physical storage. Điểm này không còn là ý tưởng quá riêng của Databricks nữa: Unity Catalog OSS, Apache Gravitino và một số catalog hiện đại khác cũng đang đi theo hướng đưa authN/authZ, metadata và credential policy lên catalog layer thay vì để từng engine tự xử lý rời rạc.

Bởi vì storage không biết “table” là gì. Nó chỉ thấy bucket, object và path. Nếu table sales nằm tại s3://bucket/warehouse/sales/, Databricks phải cho user đọc đúng path đó mà không mở quyền trên toàn bộ bucket.

Unity Catalog giải bài toán này bằng cách derive quyền từ logical object xuống physical path, rồi cấp temporary credential có scope và thời hạn phù hợp. Pattern này cũng đã xuất hiện trong open-source catalog ecosystem: UC OSS cũng có server configuration/API surface và temporary credential API/connector path, còn Apache Gravitino cũng có credential vending theo hướng để catalog đứng giữa engine và storage credential.

SELECT on table
  → table/path access derivation
  → temporary scoped credential
  → access the authorized path

Nhờ vậy, user không cần giữ IAM permission trực tiếp trên bucket, còn cluster cũng không phải dùng một credential rộng cho mọi truy cập. Permission được quản tập trung trong Unity Catalog, nhưng được enforce tiếp ở storage boundary bằng credential ngắn hạn. Nếu credential có thể dùng mãi hoặc dùng lại bên ngoài runtime, user vẫn có thể bypass Unity Catalog.

The Fourth Requirement: Every Filesystem Path Must Be Controlled

Ta vừa kiểm soát đường truy cập dữ liệu đi qua Spark logical plan. Nhưng notebook vẫn còn những đường khác: user có thể gọi FileSystem trực tiếp, dùng một implementation cụ thể, hoặc đơn giản là gọi Python open() trên một POSIX path.

Nếu các đường này chạm thẳng vào storage hoặc file nội bộ của runtime, chúng có thể bypass Unity Catalog. Vì vậy, Databricks phải biến filesystem thành một controlled interface, không phải một local filesystem thuần.

Ở tầng Spark, các scheme trong core-site.xml được redirect sang implementation của Databricks, ví dụ LokiFileSystem cho nhiều cloud storage scheme và WorkspaceLocalFileSystem cho một số đường file://. Nhưng config redirect chưa đủ, vì JVM code vẫn có thể cố gọi thẳng class gốc như RawLocalFileSystem. Snapshot này cho thấy Databricks còn dùng safety JAR/classpath shadow để kiểm call site trước khi cho phép truy cập local filesystem.

Ở tầng OS/runtime, các FUSE mount như /dbfs, /Volumes/Workspace giúp user vẫn dùng path quen thuộc với pandas.read_csv() hoặc open(), nhưng phía sau path đó có thể là service boundary chứ không phải local disk. Pattern này khá giống các hệ thống mount storage vào runtime như DataShim hoặc object-store FUSE: app nhìn thấy một path quen thuộc thay vì gọi storage API trực tiếp. Điểm khác là trong Databricks, mount path không chỉ phục vụ convenience; nó còn nằm trong security/runtime boundary của Unity Catalog, temporary credentials và workspace services.

Đây cũng là lý do runtime phải cẩn thận với những hành vi tưởng vô hại như Python import scan qua Workspace Filesystem: một opendir có thể kéo theo remote call tới control plane.

Architecture Components

Các requirement ở trên cho ta một intention map: interactive runtime không chỉ là một Spark Driver có notebook gắn vào. Nó phải provision được runtime cho nhiều user, tách user code khỏi control path, enforce authorization ở logical plan, rồi tiếp tục siết xuống filesystem và storage credential.

Hiểu được bài toán rồi thì bước tiếp theo là quay lại các component cơ bản: chúng là gì, nằm ở đâu trong runtime, và thực sự làm gì. Nhìn chung, kiến trúc này được tớ phỏng đoán dựa trên observation trên một runtime snapshot, chắc chắn không thể tránh khỏi thiếu và sai sót. Tuy nhiên ta vẫn sẽ hình dung được cơ bản các thành phần chính.

DriverDaemon and DriverCorral

Driver-side architecture reconstructed from process, JVM and REPL signals

Như đã đề cập trước đó, Databricks không tạo một Spark Driver mới cho mỗi command hay mỗi notebook. Thay vào đó, nó duy trì một Driver runtime sống dài rồi đặt lên trên runtime này một lớp quản lý các interactive sessions. Tuy nhiên điểm thực sự thú vị ở đây là Databricks không hề đụng vào core của Spark, người ta làm bằng phương pháp thú vị hơn rất nhiều.

Spark runtime bên dưới vẫn được khởi tạo theo mô hình gần với spark-shell: một Spark application mang tên Databricks Shell giữ singleton SparkContext được tạo ra bởi DatabricksILoop (implementation của DriverILoop - Scala REPL, kế thừa từ SparkILoop hay spark-shell) và giữ nó tồn tại lâu dài. Vì vậy, dưới góc nhìn của Spark, compute vẫn đang chạy một application bình thường; điểm khác biệt chủ yếu nằm ở lớp lifecycle và session management mà Databricks xây dựng xung quanh application đó.

SparkContext dùng chung được wrapped bởi SharedDriverContext. Lớp này cung cấp access point ổn định để các thành phần khác reference hoặc init Spark environment mà không trực tiếp sở hữu lifecycle của nó. Đây là nền tảng dùng chung của toàn bộ runtime, chứ không thuộc riêng một notebook nào.

Bao quanh Spark environment đó là DriverDaemon, một JVM được Chauffeur khởi chạy và giám sát như một process riêng. Vai trò chính của DriverDaemon không phải giữ SparkContext hay trực tiếp thực thi notebook command, mà là bootstrap toàn bộ driver-side runtime: lắp ráp các component, mở những internal server và communication channel cần thiết, rồi điều khiển quá trình start hoặc stop của chúng. Có thể xem DriverDaemon như điểm khởi đầu nơi runtime được dựng lên, còn state thực sự được giữ bởi những component chuyên biệt phía sau.

Một trong các component quan trọng nhất được DriverDaemon khởi tạo là DriverCorral. Trong khi SharedDriverContext đại diện cho Spark environment dùng chung, DriverCorral quản lý phần interactive execution (các DriverLocal): tiếp nhận command, tạo execution context và điều phối các REPL hoặc session đang hoạt động.

SharedDriverContextDriverCorral được giữ qua hai lifecycle holder riêng, vì chúng không nhất thiết phải lúc nào cũng sẵn sàng và sẵn sàng cùng thời điểm. Trong khi Spark environment có thể chỉ được khởi tạo khi thực sự cần,các routing server phải mở cổng sớm hơn để nhận request. Vì vậy, DriverCorral được đặt sau ReadinessAwareDriverCorral, một wrapper kiểm tra Corral đã được gắn vào và đã sẵn sàng hay chưa trước khi chuyển tiếp request. Cơ chế này cho phép runtime bootstrap theo nhiều phase mà không để request đi vào một execution layer mới chỉ được dựng một phần.

Spark environment cũng có thể được khởi tạo theo hướng lazy. DriverCorral lấy SharedDriverContext khi execution path thực sự cần tới nó, thay vì DriverDaemon tạo sẵn toàn bộ Spark state rồi truyền xuống. Bởi vậy, trạng thái “Driver service đã chạy” không nhất thiết đồng nghĩa với “SparkContext đã được tạo và sẵn sàng”. Sự tách biệt này giúp giải thích vì sao runtime cần các phase bootstrap và readiness signal riêng.

Khi một notebook session được tạo, DriverCorral cấp phát một DriverLocal tương ứng với execution mode hoặc ngôn ngữ: ScalaDriverLocal quản lý Scala REPL, PythonDriverLocalBase giao tiếp với Python process, còn SQL và R có các implementation riêng. Mỗi DriverLocal giữ session state, SQLContext và execution identity của phiên làm việc mà nó đại diện.

Như vậy, ranh giới chia sẻ và cô lập nằm ở hai tầng khác nhau và cũng có thể là một phần lý do là Databricks đặt tên dịch vụ này là All-purpose Cluster. Các notebook có execution context và session state riêng, nhưng cuối cùng vẫn sử dụng chung một SparkContext và một Spark application sống dài. ó là cơ chế cốt lõi giúp Databricks cung cấp cảm giác mỗi notebook có một môi trường tương tác riêng, trong khi tài nguyên Spark phía dưới vẫn được dùng chung ở cấp compute.

Một điểm thú vị là bên trong Driver này còn có chạy sẵn một SparkConnectService để phục vụ kết nối qua SparkConnect từ bên ngoài binding trên port 15002; khả năng cao là luồng proxy riêng, phục vụ databricks-connect vì chính services còn tự Authenticate user từ xa, không qua notebook. REPL cũng có nhiều lựa chọn SparkMode như DRIVER, CONNECT, REMOTE_CONNECT hay PIPELINE_UDS_CONNECT. Nghĩa là khả năng Databricks đang dùng một runtime cho nhiều môi trường, dịch vụ khác nhau và chúng chỉ là biến thể dựa trên nhu cầu.

Chauffeur

Nếu DriverDaemon là cái vỏ của Spark driver sống dài, thì Chauffeur là người đứng bên ngoài để chăm cái vỏ đó. Nó được khởi động trước bởi dbr_entry_point.py, mở các endpoint để Control Plane nói chuyện với cluster, rồi chịu trách nhiệm dựng, theo dõi và điều phối request xuống driver.

Vai trò đầu tiên của Chauffeur khá dễ hiểu: supervisor. Nó chạy start_driver.sh để sinh ra DriverDaemon, giữ PID của driver, theo dõi driver theo chu kỳ và có thể chẩn đoán hoặc restart khi driver bị treo. Vì Chauffeur là một process riêng, nó vẫn còn sống ngay cả khi Driver JVM bị lỗi. Đây là lý do phần chăm sóc driver không thể chỉ là một thread nằm bên trong chính driver.

Vai trò thứ hai là cổng điều phối session và command. Khi user attach một notebook, Chauffeur không tạo Spark driver mới. Nó tạo một session ở tầng điều phối, gắn session đó với một ExecutionContextId, rồi gửi yêu cầu xuống DriverCorral để start một REPL tương ứng. Ở trong driver, DriverCorral dùng ExecutionContextId đó để dựng DriverLocal cho đúng notebook/language, còn Spark runtime gốc bên dưới vẫn là phần dùng chung.

notebook attach
  → Chauffeur session / ExecutionContextId
  → DriverCorral.startRepl(...)
  → DriverLocal / REPL riêng cho session
  → shared SparkContext bên dưới

Phần session-specific config có tồn tại: snapshot có ConfigCategory.SESSIONdynamicSparkConfig.conf.json chủ yếu là config ở scope session. Nhưng trên đường notebook, identity/token không nằm trực tiếp trong body của StartRepl hay ExecuteCommand, và cũng không thấy nó được pass xuống bằng một SparkConf riêng trong message này. Các message này chủ yếu mang ReplId, ExecutionContextId, language và command cần chạy.

Identity đi bằng một đường khác: nó đã được xác thực ở phía ngoài rồi được propagate như UserContext/Ctx của request. Khi command thật sự chạy, driver mới dùng context đó để setup token/credential, kiểm quyền và truyền tiếp user context xuống executor khi cần. Vì vậy có thể hiểu gần đúng là: mỗi notebook có session/execution context và REPL riêng, có session config riêng, nhưng cùng trỏ vào một Spark driver gốc; identity không phải là một field trong command, mà là context đi kèm execution path.

SparkMaster

Databricks vẫn dùng Spark Standalone cho từng cluster. Trong Driver Container có một SparkMaster lắng nghe ở :7077, được khởi động bằng dbr_entry_point.py ngay sau Chauffeur. Driver vẫn nối vào nó bằng StandaloneAppClient, Worker register vào nó, rồi Master launch Executor cho Spark application.

Trong thực tế, vai trò lõi của Master vẫn gần giống cluster manager của Spark Standalone: nhận Worker, launch Executor và theo dõi trạng thái Worker/Executor cho một Spark application. Nó không nhận notebook command, không quản session, và không phải nơi quyết định user code chạy thế nào. Điểm khác là Databricks đặt Master này vào một vòng điều khiển lớn hơn của platform: autoscale, decommission, warmup và recovery được quyết định hoặc điều phối ở phía ngoài Spark, còn Master là nơi Worker/Executor được gắn vào hoặc tháo ra khỏi application.

Vì vậy, Auto-Scale trong Databricks không phải là Spark Dynamic Allocation tự quyết toàn bộ. Driver/Chauffeur cung cấp signal về workloads, ví dụ backlog task, tổng task slot, trạng thái command và load của interactive runtime. Control Plane mới là nơi nhìn cluster ở cấp platform rồi quyết định provision thêm VM/container hoặc decommission Worker. Sau khi Worker xuất hiện, nó register vào SparkMaster; khi cần scale down, Master nhận lệnh decommission. Nói cách khác, Master tham gia vào vòng scale, nhưng nó không phải autoscaling brain. Nó vẫn chủ yếu là attachment point cho Worker/Executor của Spark application.

Other Implementations

Ngoài các process lớn như Chauffeur, DriverDaemon, DriverCorral và SparkMaster, runtime còn có nhiều implementation nhỏ hơn nhưng rất quan trọng. Chúng không tạo thành một service riêng, nhưng lại là chỗ Databricks cắm thêm boundary vào Spark runtime.

Trường hợp dễ thấy nhất là filesystem. Trong Spark/Hadoop bình thường, s3a://, abfss://, dbfs:// hay file:// sẽ được resolve tới các Hadoop FileSystem implementation tương ứng. Trong snapshot này, nhiều scheme cloud storage được redirect về implementation của Databricks, nổi bật là LokiFileSystem; một số đường local/workspace cũng đi qua WorkspaceLocalFileSystem thay vì raw local filesystem. Ý chính không phải là đổi tên class cho vui, mà là biến filesystem thành một controlled interface: user vẫn nhìn thấy path quen thuộc, nhưng phía sau path đó có thể đi qua Unity Catalog, credential scope, workspace service hoặc rule kiểm soát local filesystem.

Permission check cũng đi theo cùng pattern: không dựng một service authZ riêng nằm ngoài Spark rồi hy vọng mọi thứ sẽ gọi qua đó. Conceptually, nó giống các open-source stack như Ranger ở chỗ đều muốn chặn trên analyzer path: query phải thành logical plan rồi mới chạy, nên đây là điểm tự nhiên để kiểm quyền. Khác biệt nằm ở implementation. Ranger/open-source thường cắm rule qua extension point như spark.sql.extensions; còn trong snapshot Databricks này, authZ không đi bằng extension user-configurable kiểu đó. Databricks cắm seam vào bên trong Spark fork, ở tầng build SessionState/analyzer, qua các hook như SessionStateBuilder/DatabricksEdge. Các rule như CheckPermissions hoặc CheckFileSystemSafety vì vậy nằm trên đường chung của Python, Scala, SQL và R: dù user gọi bằng API nào, cuối cùng query vẫn phải thành logical plan và bị kiểm ở cùng một chỗ.

Nhìn rộng ra, nhiều implementation trong runtime này có cùng một ý tưởng: giữ API nhìn từ phía user tương đối quen thuộc, nhưng thay implementation bên dưới bằng bản có kiểm soát. Spark vẫn có Master, Driver, Executor, SparkContext, Hadoop FileSystem, analyzer rule; nhưng Databricks thay hoặc bọc các điểm seam quan trọng để thêm session isolation, permission check, filesystem boundary, credential passthrough và autoscaling signal. Đây là khác biệt lớn giữa "chạy Spark trong notebook" và biến Spark thành một interactive managed runtime.

Lakeguard Paper Cross-check

Sau khi viết xong phần này, tớ tình cờ tìm thêm được paper Databricks Lakeguard: Supporting Fine-grained Access Control and Multi-user Capabilities for Apache Spark Workloads. Paper đó mô tả bài toán ở tầng chính thức hơn: chuyển Spark từ mô hình cluster-bound permission sang user-bound governance, tách client/user code khỏi trusted Spark engine, rồi dùng Unity Catalog để enforce permission và credential theo identity.

Điểm làm tớ thấy đáng chú ý là fine-grained access control trong Databricks không phải lúc nào cũng chỉ chạy local trên cluster hiện tại. Với Standard compute, vì user code đã được sandbox khỏi core engine, row filter/column mask có thể được enforce local. Nhưng với Dedicated hoặc privileged compute, một số phần FGAC có thể được rewrite và route sang Serverless Spark/eFGAC để xử lý trong một runtime đủ trusted hơn.

Điều này làm những gì tớ phỏng đoán ở trên đáng tin hơn: các component trong snapshot như Chauffeur, DriverCorral, SafeSpark, filesystem wrapper, UC credential boundary và Spark Connect service đều có thể đọc như các implementation cụ thể của cùng một intention Lakeguard đó.

My Summary

Sau khi bóc tới đây, cảm giác đầu tiên của tớ là: runtime này không phải một thiết kế được nghĩ ra trong một lần. Nó giống kết quả của nhiều năm tích tụ design, patch, constraint và production incident hơn. Có quá nhiều lớp nằm chồng lên nhau: process lifecycle, REPL/session management, identity, permission, filesystem, credential, autoscaling, warmup, recovery. Thật lòng mà nói, nếu không có AI Agents hỗ trợ đọc, tìm, nối evidence và giữ context, phần này hơi quá sức với tớ.

Nhưng chính vì vậy nó cũng làm rõ một điều: đây không phải kiểu “customize vài open-source component là xong”. Databricks vẫn dùng rất nhiều mảnh quen thuộc của Spark và Hadoop, nhưng họ phải mạnh tay thay, bọc hoặc patch vào những seam quan trọng bên trong runtime. Đọc thêm Unity Catalog OSS, Apache Gravitino và các catalog/open lakehouse project mới hơn cũng làm tớ thấy nhiều pattern như authN/authZ ở catalog layer, temporary credential vending hay storage credential boundary đang dần trở thành building block chung của ecosystem. Vì vậy, cái khó của Databricks không chỉ nằm ở từng mảnh riêng lẻ, mà ở việc tích hợp chúng đủ sâu vào một managed interactive runtime/product chạy được trong production.

Ban đầu nhìn vào snapshot này rất rối. Tên class thì nhiều, process thì nhiều, config thì dày, proto/log/event thì gần như ở mọi chỗ. Nhưng bóc dần ra thì bắt đầu thấy được bản đồ: cái gì là Spark gốc, cái gì là wrapper, cái gì là fork seam, cái gì là platform boundary, cái gì chỉ là implementation detail. Từ chỗ bơi giữa một đống artifact hỗn độn, ít nhất bây giờ tớ có một mental model để tiếp tục đọc tiếp.

Dĩ nhiên đây mới chỉ là một lát cắt nhỏ. Còn rất nhiều thứ cần đào sâu hơn: authZ trong Spark fork, credential passthrough, filesystem wrapper, SafeSpark, Chauffeur RPC, Spark Connect, autoscaling, warmup/SnapStart, và sự khác nhau giữa classic/serverless runtime. Nhưng phần quan trọng nhất là tớ đã bắt đầu thấy được conceptual intention phía sau: Databricks không chỉ chạy Spark cho user, mà biến Spark thành một runtime tương tác, multi-user, managed và có kiểm soát.

On this page