Databricks Interactive Runtime
Tìm hiểu cơ chế Databricks Interactive Cluster: vì sao notebook không phải Driver, và Databricks đã giải quyết bài toán client-server Spark runtime từ rất sớm.
Problem Statement
Đã từ rất lâu, mình luôn tò mò về một câu hỏi:
Làm thế nào Databricks có thể duy trì một Spark Cluster (Driver + Executors) chạy liên tục trong nhiều ngày để phục vụ Interactive Workloads, nhưng vẫn cho phép nhiều notebook của nhiều người dùng cùng thao tác trên một cụm Spark duy nhất?
Nếu từng làm việc với Spark thuần, có lẽ sẽ thấy đây là một điều khá "kỳ lạ". Trong Spark truyền thống, Driver gần như luôn gắn chặt với vòng đời của một Spark Application. Driver được tạo khi application bắt đầu và bị hủy khi application kết thúc. Trong khi đó, Databricks lại hoạt động hoàn toàn khác:
- Cluster được tạo trước và có thể chạy liên tục nhiều ngày.
- Driver JVM cũng tồn tại xuyên suốt vòng đời của Cluster.
- Nhiều notebook có thể đồng thời attach vào cùng một Driver.
- Người dùng vẫn có đầy đủ khả năng sử dụng các API JVM như SparkContext, RDD, SparkSession, TempView...
- Các notebook vừa có trạng thái (state) riêng, vừa có khả năng chia sẻ state của Spark Runtime.
Đây gần như là một mô hình lai giữa Spark Client Mode và Spark Cluster Mode nhưng lại mang nhiều đặc điểm của kiến trúc client-server. Sau khi Spark Connect ra đời, mình mới nhận ra Spark cũng đang đi theo hướng client-server tương tự. Tuy nhiên, Databricks đã giải quyết bài toán Interactive Workloads theo cách riêng của họ từ rất lâu trước đó, và kiến trúc bên dưới có vẻ phức tạp hơn khá nhiều.
Điều thú vị là Databricks gần như không công khai toàn bộ cơ chế runtime này. Vì vậy, ngoài việc đọc tài liệu, mình muốn quan sát trực tiếp trên Driver Node để hiểu các thành phần bên dưới vận hành như thế nào.
Traditional Spark vs Databricks Interactive Cluster
Để hiểu Databricks khác biệt ở đâu, trước tiên cần nhìn lại cách Spark hoạt động trong mô hình truyền thống.
Traditional Spark: Driver Tied to Application
Trong Spark, về bản chất luôn tồn tại một Driver điều khiển toàn bộ Application. Người dùng thường chạy Spark theo hai mô hình chính.
Client Mode
Đây là mô hình phổ biến cho các interactive workload. Ở đó, runtime khởi tạo SparkSession cũng chính là Driver. Ví dụ PySparK, Jupyter Notebook hay Airflow sử dụng spark-submit --deploy-mode client. Trong trường hợp này Python Process chứa user code, Process đó tạo SparkSession và JVM Driver được khởi tạo bên trong process. Nếu process bị kill, Driver cũng mất theo và toàn bộ Spark Application kết thúc.
Jupyter client mode
Ví dụ với Jupyter chạy Spark theo client mode, mỗi notebook/kernel thường tự tạo Spark runtime riêng của nó:
Trong mental model đơn giản này, mỗi notebook tương ứng với một Driver riêng. Nếu Python kernel của notebook bị kill, Driver gắn với notebook đó cũng mất theo.
Cluster Mode
Ở Cluster Mode, client không trực tiếp chạy Driver. Thay vào đó:
- Client submit Application lên Cluster Manager (YARN, Kubernetes, Standalone...)
- Cluster Manager tạo Driver.
- Driver request Cluser Manager tạo Executors.
- Khi Application kết thúc, toàn bộ Driver và Executors cũng bị giải phóng.
Spark cluster mode
Ở cluster mode, client chỉ submit application. Driver được tạo bởi Cluster Manager và sống trong cluster thay vì sống trực tiếp trong client process:
Driver vẫn chỉ sống cùng vòng đời của Application. Chi tiết hơn bạn có thể tham khảo tại So sánh YARN và K8S trong quá trình cấp phát resource cho Spark.
Common Runtime Pattern
Dù là Client Mode hay Cluster Mode thì kiến trúc cơ bản vẫn giống nhau.
Spark driver runtime layout
Dù là Python/PySpark hay Scala, user code cuối cùng vẫn đi vào một JVM Spark Driver sở hữu các thành phần chính như SparkSession, SparkContext, Catalyst và Scheduler.
Điểm khác nhau là Python phải đi qua Py4J để gọi JVM, còn Scala đã chạy trong JVM ecosystem nên gọi trực tiếp vào Spark APIs hơn. Nhưng về mặt application lifecycle, cả hai vẫn xoay quanh một JVM Driver cho mỗi Spark Application.
Hay nói cách khác:
Một Spark Application gần như luôn đồng nghĩa với một Driver.
Databricks Interactive Cluster
Trong Databricks, interactive workload hiện nay chủ yếu rơi vào hai nhóm compute lớn:
- All-purpose cluster: cluster được tạo trước để notebook attach vào và chạy tương tác.
- Serverless compute: Databricks quản lý runtime phía dưới nhiều hơn, người dùng gần như chỉ quan tâm tới notebook/session.
Riêng All-purpose cluster lại có nhiều access mode khác nhau. Với mục tiêu hiểu cơ chế runtime, tớ muốn tách rõ ba trường hợp:
| Compute | Mode | Runtime intuition | Vai trò trong note này |
|---|---|---|---|
| All-purpose | Dedicated | Support đầy đủ nhất cho Spark/JVM APIs truyền thống; Databricks docs nêu Dedicated nên dùng khi cần RDD APIs, GPU, R language hoặc privileged machine access. Dedicated compute có thể assign cho single user hoặc single group, nên nếu dùng group assignment thì nhiều user trong group đang cùng dùng chung một compute boundary. | Trọng tâm nghiên cứu chính, vì runtime sharing phía Driver dễ quan sát nhất. |
| All-purpose | Standard | Có nhiều limitation hơn: R không được support, RDD APIs không được support, Spark Connect được dùng trong các Databricks Runtime mới hơn; Python thường đi theo Spark Connect boundary, còn Scala có thể nhìn như Classic org.apache.spark.sql.SparkSession nhưng vẫn bị Databricks giới hạn API surface, đặc biệt sc, spark.sparkContext, sqlContext không được support cho Scala. | Sẽ brief lại như một case “restricted classic-looking session”, nhưng không đào sâu bằng Dedicated. |
| Serverless | — | Databricks che nhiều runtime detail hơn; với Python notebook thường biểu hiện bằng pyspark.sql.connect.session.SparkSession, tức Spark Connect/client-server boundary rõ hơn. Không dùng nó làm môi trường chính để khảo sát Driver/Py4J/SparkContext internals. | Sẽ brief lại để so sánh. |
Tham khảo chính thức
Các limitation/access-mode ở bảng trên dựa trên Databricks docs:
Các điểm quan trọng từ docs: Standard compute không support R, không support RDD APIs, và sc / spark.sparkContext / sqlContext không được support cho Scala. Một số Scala/Spark API support phụ thuộc Databricks Runtime version. Dedicated compute được khuyến nghị cho RDD APIs, GPU, R language support và privileged machine access, đồng thời có thể assign cho một user hoặc một group.
Tuy nhiên, docs chỉ nói Dedicated compute có thể assign cho single user hoặc single group. Phần mình cần kiểm chứng thêm là: nếu một Dedicated compute được assign cho group, thì mọi user trong group có đang đi qua cùng một long-running Driver/runtime boundary hay không. Nếu đúng, khả năng cao nhiều thứ như Spark config, temp view hoặc một số runtime-side state sẽ là shared theo compute/group boundary, chứ không phải passthrough hoàn toàn theo từng user. Đây là một lý do nữa khiến Dedicated mode thú vị hơn để khảo sát cơ chế runtime.
Phần làm tớ thấy thú vị nhất là All-purpose Dedicated mode. Ở đây, cluster được tạo trước với một Driver và nhiều Executors, có thể sống nhiều giờ hoặc nhiều ngày. Trong khoảng thời gian đó, nhiều notebook có thể attach vào cùng một cluster, Spark runtime vẫn được giữ nguyên, và nhiều trạng thái như Temp View, Global Temp View, SparkSession/SparkContext có thể tồn tại xuyên suốt runtime.
Điều này đặt ra hàng loạt câu hỏi thú vị:
- Nếu Driver chỉ có một thì làm sao nhiều notebook có thể cùng thực thi?
- Notebook được cô lập ở đâu?
- Python runtime được tổ chức như thế nào?
- Vì sao notebook có biến Python riêng nhưng vẫn có thể chia sẻ Spark-side state?
- Driver quản lý đồng thời nhiều notebook bằng cơ chế gì?
Với Standard mode và Serverless, tớ sẽ brief lại sau bằng code ví dụ và so sánh với Spark Connect, nhưng chúng không phải trọng tâm chính của phần tìm hiểu cơ chế runtime này. Trọng tâm chính ở đây là Dedicated mode, vì nó giữ lại nhiều dấu vết runtime truyền thống hơn để quan sát trực tiếp trên Driver Node.
Runtime Surface by Compute and Language
Để tránh trộn lẫn compute mode với language frontend, phần này tách thành 6 tổ hợp compute × language. Cách kiểm tra nhanh nhất là chạy trực tiếp biến spark trong notebook, rồi so sánh output với compute selector ở góc trên bên phải.
| Compute | Language | Support | spark / runtime surface | Boundary đáng chú ý |
|---|---|---|---|---|
| Serverless | Python only | Có Python, không dùng cho Scala trong scope này | pyspark.sql.connect.session.SparkSession | Spark Connect / client-server rõ ràng; dùng làm contrast với All-purpose |
| All-purpose Standard | Python | Có | pyspark.sql.connect.session.SparkSession | Spark Connect / client-server rõ ràng |
| All-purpose Standard | Scala | Có, nếu Databricks Runtime đạt yêu cầu | classic-looking org.apache.spark.sql.SparkSession | Nhìn giống Spark classic nhưng API surface bị giới hạn; sparkContext/sc không được support theo docs |
| All-purpose Dedicated | Python | Có | PySpark classic / Py4J-style surface | Gần Spark truyền thống hơn, có nhiều dấu vết để khảo sát Driver runtime |
| All-purpose Dedicated | Scala/R | Có | Spark/JVM hoặc R runtime truyền thống hơn | Support rộng nhất; phù hợp nhất nếu cần SparkContext/RDD/JVM APIs hoặc R language |
Serverless Compute
Serverless trong note này chỉ dùng như một contrast case cho Python/Spark Connect, không tách riêng Scala nữa. Khi chạy trực tiếp biến spark trong notebook với Serverless Compute, ta thu được kết quả như sau:

Python cell chạy spark trả về pyspark.sql.connect.session.SparkSession, trong khi Scala cell bị reject với lỗi [UNAUTHORIZED_COMMAND] Scala is not yet supported by Serverless compute.
Điều này cho thấy Serverless Compute chỉ có thể giữ một client-side session object và nói chuyện với Spark runtime phía sau qua boundary của SparkConnect, kiểu client-server.
All-purpose Compute with Dedicated Mode
Ngược lại Serverless, All-purpose Dedicated là môi trường gần với Spark classic truyền thống hơn, cả với Python lẫn Scala/R.

Screenshot này chạy cùng một probe trên hai notebook khác nhau cùng attach vào một All-purpose Dedicated compute. Ở đây mình chỉ tập trung vào ba boundary: Spark application, SparkContext, và SparkSession. Phần visibility của temp view/global temp view sẽ phân tích riêng sau.
Các tín hiệu chính từ output:
-
Hai notebook chung Spark application: Cả hai notebook đều in ra
application_id = app-20260731062159-0000. Vì vậy ở mức application boundary, evidence hiện tại cho thấy hai notebook đang attach vào cùng một Spark application. -
Hai notebook chung JVM
SparkContext: Cả hai notebook đều nhìn thấy cùng referenceorg.apache.spark.SparkContext@45c4c4ca.SparkContextlà object lõi của Spark application, nó giữ kết nối tới scheduler, executors, Spark UI, application state và runtime config ở tầng SparkContext. ViệcSparkContextreference giống nhau mạnh hơn việc chỉ giốngapplicationId, vì nó chỉ ra hai notebook đang nhìn về cùng object SparkContext trong Driver JVM. Nhận định hợp lý có thể là Dedicated compute giữ một Driver runtime / SparkContext chung, rồi cho nhiều notebook attach vào runtime đó. -
Hai notebook không chung JVM
SparkSession: Notebook 1 dùngorg.apache.spark.sql.classic.SparkSession@78698f5bvà Notebook 2 dùngorg.apache.spark.sql.classic.SparkSession@531a586a. Scala identity hash cũng khác nhau:2021838399vs1394235498. Vì vậy không nên viết là “hai notebook dùng chung SparkSession”. Cách đọc đúng hơn là: chúng chung Spark application và SparkContext, nhưng mỗi notebook có một SparkSession riêng nằm trên cùng SparkContext đó. -
Python và Scala trong cùng notebook lại chung session object: Ở Notebook 1, Python
jvm_spark_sessionvà Scalasparkcùng trỏ tớiSparkSession@78698f5bvà ở Notebook 2, Pythonjvm_spark_sessionvà Scalasparkcùng trỏ tớiSparkSession@531a586a. Điều này gợi ý SparkSession boundary nằm theo notebook, không nằm theo language cell. Một notebook có thể có nhiều language frontend, nhưng các language cell đó bridge tới cùng JVM SparkSession của notebook đó. -
Python có frontend wrapper, nhưng Scala reflection lại cho kết quả ngược lại.
- Python
spark.sparkContexthiện làRemoteContexttrongdbruntime.spark_connection..., nên Python objectspark.sparkContextkhông phảiorg.apache.spark.SparkContextclassic trực tiếp. Tuy nhiên, khi Python đi qua bridge để lấy JVM reference, nó vẫn resolve về cùngSparkContext@45c4c4camà Scala nhìn thấy. - Khi probe bổ sung cho Scala, tớ nhận thấy
sparklàorg.apache.spark.sql.classic.SparkSession, superclass làorg.apache.spark.sql.SparkSession, vàspark.sparkContexttrả về đúngorg.apache.spark.SparkContext. Method surface của ScalaSparkSessioncũng exposesparkContext: org.apache.spark.SparkContextvàsqlContext. Nghĩa là trong Dedicated case này Scala không có dấu hiệu đi quaRemoteContextwrapper giống Python; Scala đang nhìn thấy Spark classic JVM objects trực tiếp hơn.
- Python
Thí nghiệm sâu hơn với spark.conf, Local Temporary View và Global Temporary View cho thấy Dedicated không “share hết mọi thứ”. Các notebook attach vào cùng một cluster có thể share chung Global Temporary View, nhưng không share chung spark.conf và local temp view. Điều này củng cố nhận định rằng các notebook dùng chung Spark application / SparkContext, nhưng khác SparkSession.
Ngược lại, các ngôn ngữ trong cùng một notebook lại share được cả config và Local Temporary View. Python cell và Scala cell cùng đọc được state do notebook đó tạo ra, nên trong case này chúng không phải hai session độc lập; chúng cùng bridge tới SparkSession của notebook hiện tại.
Từ đó đi đến kết luận như sau:
SparkContext/ Spark application là shared boundary. Hai notebook cùng nhìn thấy mộtapplicationIdvà một JVMSparkContext, nên chúng không phải hai Spark application riêng.SparkSessionlà notebook-level boundary. Mỗi notebook có một JVMSparkSessionriêng, và các state gắn với session không leak sang notebook khác.spark.conflà session-scoped trong thí nghiệm này. Config set ở Notebook 1 đọc được trong Notebook 1, nhưng Notebook 2 không thấy.- Local Temporary View là session-scoped. View tạo bằng
CREATE TEMP VIEWở Notebook 1 không visible trong Notebook 2. - Global Temporary View là application/shared-state scoped. View tạo bằng
CREATE GLOBAL TEMP VIEWở Notebook 1 visible trong Notebook 2 qua databaseglobal_temp. - Language frontend không phải SparkSession isolation boundary, nhưng vẫn là language runtime boundary. Python và Scala trong cùng notebook cùng bridge tới SparkSession của notebook đó, nên cùng thấy các state gắn với SparkSession như config và local temp view. Nhưng điều này không có nghĩa Python/Scala share chung memory hay namespace; biến Python không thể xuất hiện trong Scala, và Scala
val/varcũng không thể xuất hiện trong Python.
Mental model sau thí nghiệm state visibility:
All-purpose Dedicated compute
│
├── shared Driver JVM / Spark application
│ ├── applicationId = app-20260731062159-0000
│ ├── SparkContext@45c4c4ca
│ └── global_temp views
│
├── Notebook 1 SparkSession@78698f5b
│ ├── Python frontend
│ ├── Scala frontend
│ ├── spark.conf["probe.session.key"] = from_notebook_1
│ └── local temp view dedicated_probe_local
│
└── Notebook 2 SparkSession@531a586a
├── Python frontend
├── Scala frontend
├── own spark.conf, không thấy config Notebook 1
└── own local temp view namespace, không thấy local temp view Notebook 1Câu kết luận tạm thời: Dedicated shared compute không có nghĩa là mọi thứ đều shared. Nó shared ở Spark application / SparkContext layer, nhưng isolate ở SparkSession layer. Python và Scala chỉ là hai frontend khác nhau; notebook-level SparkSession mới là boundary chính cho session state.
All-purpose Compute with Standard mode
All-purpose Standard cần cẩn thận hơn Dedicated vì surface của nó phụ thuộc runtime. Python trên Standard thường đã đi qua Spark Connect, nhưng Scala thì không phải lúc nào cũng giống nhau giữa các runtime.
Ở Python, biến spark hiện rõ là Connect session:
<pyspark.sql.connect.session.SparkSession ...>Còn với Scala, surface thay đổi theo Spark major version trong runtime mình test. Trên runtime Spark 3.5, Scala trả về SparkSession classic:
org.apache.spark.sql.SparkSession = org.apache.spark.sql.SparkSession@78b3efebTrên runtime Spark 4.0, Scala trả về Connect session:
org.apache.spark.sql.SparkSession = org.apache.spark.sql.connect.SparkSession@6274b1e6Vì vậy không nên kết luận “Standard Scala luôn Connect” hay “Standard Scala luôn classic”. Dúng hơn là All-purpose Standard có language surface phụ thuộc Spark runtime version. Python đã rất rõ là Connect-style, còn Scala trên Spark 3.5 có thể hiện classic-looking org.apache.spark.sql.SparkSession, trong khi Scala trên Spark 4.0 có thể hiện org.apache.spark.sql.connect.SparkSession.
Điểm quan trọng là dù Scala Spark 3.5 có in ra org.apache.spark.sql.SparkSession, điều đó vẫn chưa đủ để kết luận nó giống Dedicated. Probe trên Standard Spark 3.5 cho thấy object đúng là org.apache.spark.sql.SparkSession, superclass là java.lang.Object, implements scala.Serializable, java.io.Closeable, org.apache.spark.internal.Logging, nhưng ta không thể access sc và spark.sparkContext fail ở compile-time:
not found: value sc
value sparkContext is not a member of org.apache.spark.sql.SparkSessionReflection cũng không thấy sparkContext, các method chứa context chỉ còn dạng logging/helper:
LogStringContext: org.apache.spark.internal.Logging$LogStringContext
withLogContext: voidVì vậy trong cả hai case Standard mình test, SparkContext đều không accessible. Một cách hiểu hợp lý có thể do ở Spark 3.5, Spark Connect có thể chưa đủ ready/mature cho Scala notebook runtime, nên Databricks vẫn expose class name org.apache.spark.sql.SparkSession nhưng workaround bằng cách thu hẹp method/API surface, loại sparkContext khỏi những gì user code có thể gọi. Đến Spark 4.0, surface chuyển rõ hơn sang org.apache.spark.sql.connect.SparkSession. Đây chỉ là suy luận từ behavior quan sát được, không phải kết luận chính thức từ Databricks, nhưng nó giải thích được vì sao Spark 3.5 nhìn classic còn Spark 4.0 nhìn Connect, trong khi cả hai đều không cho chạm SparkContext.
Dedicated thì ngược lại: đã được kiểm chứng bằng JVM object identity, SparkContext, applicationId, config và temp view visibility. Vì vậy class name không phải boundary quan trọng nhất, boundary quan trọng là API/capability surface mà runtime cho notebook chạm tới.
Với Standard, probe nên đi theo hai hướng. Thứ nhất là runtime surface: Python/Scala đang expose Connect session hay classic-looking session trên Spark 3.5/4.0, và method surface có còn sparkContext hay không. Thứ hai là state visibility: giữa hai notebook Standard attach cùng cluster, spark.conf, local temp view và global temp view có behave giống Dedicated hay không.
All-purpose Standard
│
├── Python frontend
│ └── pyspark.sql.connect.session.SparkSession
│
├── Scala frontend
│ ├── Spark 3.5: org.apache.spark.sql.SparkSession
│ └── Spark 4.0: org.apache.spark.sql.connect.SparkSession
│
└── Databricks-controlled boundary
└── controlled Spark runtime phía sauWorking Thesis
Cách mình đọc hiện tại là: Databricks đang cố gắng kéo Spark truyền thống từ mô hình driver/application-centric sang một mô hình gần với client-server interactive runtime hơn.
Trong Spark truyền thống, Driver thường gắn chặt với một application cụ thể. Application bắt đầu thì Driver bắt đầu, application kết thúc thì Driver cũng kết thúc. Databricks Interactive Cluster đi theo một hướng khác: Spark runtime có thể sống lâu hơn một notebook command, nhiều notebook có thể attach vào cùng một runtime phía sau, nhưng từng notebook vẫn cần có boundary riêng cho session state, command execution, cancellation và quyền truy cập API.
Dedicated cho thấy mặt “legacy/classic” của hướng đi này khá rõ. Notebook có thể quan sát runtime gần Driver/JVM hơn, thậm chí các notebook attach cùng cluster có thể chung Spark application / SparkContext. Nhưng state vẫn không phải tất cả đều shared: global_temp có thể nằm ở application/shared layer, trong khi spark.conf và local temp view vẫn nằm trong boundary của từng SparkSession/notebook.
Standard cho thấy hướng phát triển ngược lại với Dedicated: Databricks đang cố gắng thu hẹp surface mà user có thể chạm tới, và đẩy cả Python lẫn Scala về mô hình Spark Connect/client-server nếu runtime cho phép. Thay vì để notebook đi thẳng xuống Driver/SparkContext internals như Spark classic, hướng mới giữ trải nghiệm DataFrame/SQL quen thuộc nhưng đặt một boundary rõ hơn giữa user code và Spark runtime phía sau.
Vì vậy Spark Connect nên được xem như một working result nhỏ hơn của hướng kiến trúc này khi nó được đưa vào Apache Spark, chứ không phải lời giải thích đầy đủ cho toàn bộ Databricks Interactive Runtime. Spark Connect formalize mô hình client-server cho Spark, nhưng nó vẫn có hạn chế riêng: không expose trực tiếp SparkContext, không bao phủ đầy đủ RDD/JVM API surface, và không giải thích được các hành vi Dedicated vốn đã tồn tại trước Spark Connect.
Điều thú vị hơn là trajectory: mô hình ban đầu kiểu Dedicated đem lại khả năng tương thích rất mạnh với Spark classic, nhưng cũng kéo theo nhiều vấn đề isolation vì user có thể chạm sâu vào Driver/JVM/SparkContext. Các runtime mới hơn, đặc biệt ở Standard và Spark 4.0, có vẻ đang đẩy user về phía Connect-style surface nếu có thể. Đây có thể là cách Databricks giảm dần quyền truy cập trực tiếp vào internals, trong khi vẫn giữ trải nghiệm notebook/DataFrame/SQL quen thuộc.
My Summary
Điểm mình muốn giữ lại ở phần mở đầu này là: Databricks Interactive Cluster không chỉ là “Spark cluster + notebook UI”. Nó là nỗ lực biến Spark thành một interactive runtime phục vụ nhiều notebook/client, thay vì chỉ là một application chạy rồi kết thúc.
Dedicated đại diện cho giai đoạn còn rất gần Spark classic: mạnh, linh hoạt, nhưng khó cô lập tuyệt đối. Standard và Spark Connect-style surface cho thấy hướng mới hơn: giữ lại trải nghiệm Spark ở tầng DataFrame/SQL, nhưng hạn chế dần đường đi trực tiếp xuống Driver, SparkContext và JVM internals.
Vì vậy câu hỏi chính của series không phải “Databricks có dùng Spark Connect không?”, mà là:
Databricks đã từng bước chuyển Spark interactive runtime từ mô hình classic Driver-centric sang client-server như thế nào, và họ đánh đổi gì giữa compatibility, isolation và API surface?