mirror of
https://github.com/hanzoai/papers.git
synced 2026-10-22 17:20:17 +00:00
Add unified cloud binary design paper (HIP-0106) (#1)
Design paper describing the Hanzo unified Go binary architecture: one statically linked process (hanzoai/cloud) mounting every platform subsystem, built on the zip framework (Fiber v3) with embedded goja for in-process JS, esbuild for TS->ES5 transpilation, and ZAP RPC serving both the wire and the in-process dispatch seam. Covers motivation (microservices overhead for co-located control planes), architecture, the TS->goja->native-Go migration path (esign tRPC->ZAP case study, 14/14 routers server-side), multi-tenancy by configuration, the in-process-goja vs sandboxed hanzoai/runtime code-execution boundary, a cost model anchored in published service-mesh measurements, and performance (function call vs network RPC, promise pipelining). 8 pages, two TikZ figures (layered architecture, cost comparison), 20 references. Compiles clean with tectonic; no missing citations.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
\begin{figure}[t]
|
||||
\centering
|
||||
\begin{tikzpicture}[
|
||||
font=\scriptsize,
|
||||
node distance=4.5mm,
|
||||
box/.style={draw, rounded corners=2pt, align=center, minimum height=6.5mm,
|
||||
inner xsep=3pt, text width=66mm},
|
||||
layer/.style={box, fill=hzslate!8, draw=hzslate!60},
|
||||
native/.style={box, fill=hzgreen!12, draw=hzgreen!70, text width=31mm},
|
||||
js/.style={box, fill=hzred!10, draw=hzred!60, text width=31mm},
|
||||
arr/.style={-{Latex[length=2mm]}, hzslate!80, line width=0.4pt}
|
||||
]
|
||||
|
||||
\node[layer, fill=hzblue!10, draw=hzblue!70] (clients)
|
||||
{HTTP / WebSocket clients \\ \textit{SDKs, browsers, peer clusters}};
|
||||
|
||||
\node[layer, below=of clients] (router)
|
||||
{Fiber~v3 router (\texttt{zip})};
|
||||
|
||||
\node[layer, below=of router] (registry)
|
||||
{\texttt{zaprpc.Registry} \;---\; method $\rightarrow$ handler \\
|
||||
\textit{ZAP over wire $\;\parallel\;$ in-process dispatch}};
|
||||
|
||||
% two handler strategies
|
||||
\node[native, below left=8mm and -30mm of registry] (go)
|
||||
{\textbf{native Go handler} \\ function call, no serialization};
|
||||
\node[js, below right=8mm and -30mm of registry] (js)
|
||||
{\textbf{goja handler} \\ ES5 via esbuild, interpreted};
|
||||
|
||||
\node[layer, below=14mm of registry, text width=70mm, fill=hzslate!12] (subsys)
|
||||
{Subsystem APIs \;(one address space)\\[1pt]
|
||||
\footnotesize iam \,$\cdot$\, kms \,$\cdot$\, base \,$\cdot$\, gateway \,$\cdot$\, ai \,$\cdot$\,
|
||||
commerce \,$\cdot$\, vfs \,$\cdot$\, mq \,$\cdot$\, dns \,$\cdot$\, o11y};
|
||||
|
||||
\draw[arr] (clients) -- (router);
|
||||
\draw[arr] (router) -- (registry);
|
||||
\draw[arr] (registry.south) -- (go.north);
|
||||
\draw[arr] (registry.south) -- (js.north);
|
||||
\draw[arr] (go.south) -- (subsys.north);
|
||||
\draw[arr] (js.south) -- (subsys.north);
|
||||
|
||||
\begin{scope}[on background layer]
|
||||
\node[draw=hzslate!40, dashed, rounded corners=3pt,
|
||||
fit=(router)(registry)(go)(js)(subsys),
|
||||
inner sep=4pt, fill=hzslate!3] (binary) {};
|
||||
\end{scope}
|
||||
\node[hzslate!70, anchor=south east, font=\scriptsize\itshape]
|
||||
at ([xshift=-2pt,yshift=1pt]binary.north east) {one Go process};
|
||||
|
||||
\end{tikzpicture}
|
||||
\caption{The unified binary request path. Clients reach a Fiber~v3 router;
|
||||
\texttt{zaprpc.Registry} resolves each method to either a native Go handler
|
||||
or a goja-evaluated handler transpiled to ES5 by esbuild. Both terminate in
|
||||
the same subsystem APIs within one address space; the choice is invisible
|
||||
to clients and peers.}
|
||||
\label{fig:cake}
|
||||
\end{figure}
|
||||
@@ -0,0 +1,63 @@
|
||||
\begin{figure*}[t]
|
||||
\centering
|
||||
\begin{tikzpicture}[
|
||||
font=\scriptsize,
|
||||
svc/.style={draw=hzslate!60, fill=hzslate!10, rounded corners=1.5pt,
|
||||
minimum width=10mm, minimum height=5mm, inner sep=1pt},
|
||||
side/.style={draw=hzred!60, fill=hzred!12, rounded corners=1.5pt,
|
||||
minimum width=10mm, minimum height=3mm, inner sep=0pt,
|
||||
font=\fontsize{4.5}{5}\selectfont},
|
||||
one/.style={draw=hzgreen!70, fill=hzgreen!14, rounded corners=2pt,
|
||||
minimum width=44mm, minimum height=10mm, align=center},
|
||||
lbl/.style={hzslate!80}
|
||||
]
|
||||
|
||||
% --- decomposed stack (left) ---
|
||||
\node[lbl, anchor=south, font=\scriptsize\bfseries] at (1.7,2.55)
|
||||
{Decomposed: $N$ services $\times R$ replicas};
|
||||
|
||||
\foreach \x in {0,1,2,3} {
|
||||
\node[svc] (s\x) at (\x*1.15, 1.9) {};
|
||||
\node[side, below=0.4mm of s\x] (d\x) {sidecar};
|
||||
}
|
||||
\node[lbl, anchor=west, font=\fontsize{5}{6}\selectfont] at (-0.55,1.9) {};
|
||||
\node[svc, fill=hzslate!4, minimum width=48mm] (mesh) at (1.72,0.85)
|
||||
{\scriptsize service mesh control plane + mTLS per hop};
|
||||
|
||||
\draw[-{Latex[length=1.4mm]}, hzslate!70]
|
||||
(s0.east) .. controls (0.9,2.3) and (1.4,2.3) .. (s1.west);
|
||||
\draw[-{Latex[length=1.4mm]}, hzslate!70]
|
||||
(s1.east) .. controls (2.0,2.3) and (2.6,2.3) .. (s2.west);
|
||||
\draw[-{Latex[length=1.4mm]}, hzslate!70]
|
||||
(s2.east) .. controls (3.2,2.3) and (3.8,2.3) .. (s3.west);
|
||||
|
||||
\node[lbl, anchor=north, text width=52mm, align=center,
|
||||
font=\fontsize{5.5}{6.5}\selectfont] at (1.72,0.35)
|
||||
{cost $\;\propto\; N\!\cdot\!R$ containers $+\; N\!\cdot\!R$ sidecars
|
||||
$+\; h$ network hops $\times 2$ proxy traversals};
|
||||
|
||||
% --- unified binary (right) ---
|
||||
\node[lbl, anchor=south, font=\scriptsize\bfseries] at (7.6,2.55)
|
||||
{Unified: $R$ replicas};
|
||||
\node[one] (u) at (7.6,1.75)
|
||||
{one Go binary \\[1pt]
|
||||
\fontsize{5}{6}\selectfont iam $\cdot$ kms $\cdot$ gateway $\cdot$ ai $\cdot$ \dots};
|
||||
\node[lbl, anchor=north, text width=50mm, align=center,
|
||||
font=\fontsize{5.5}{6.5}\selectfont] at (7.6,1.1)
|
||||
{cost $\;\propto\; R$ containers, no sidecars, \\
|
||||
$h$ function calls, zero serialization};
|
||||
|
||||
\draw[-{Latex[length=2mm]}, hzgreen!70, line width=0.6pt]
|
||||
(4.55,1.45) -- node[above, font=\fontsize{5.5}{6}\selectfont, hzgreen!60]{collapse} (5.4,1.45);
|
||||
|
||||
\end{tikzpicture}
|
||||
\caption{Illustrative cost comparison (shape, not measurement). The
|
||||
decomposed stack pays for $N\!\cdot\!R$ containers plus an equal number of
|
||||
sidecars and a mesh control plane; each user action crossing $h$ subsystems
|
||||
pays $h$ network hops with two proxy traversals each. The unified binary
|
||||
pays for $R$ containers and turns the $h$ hops into $h$ function calls.
|
||||
Per-hop mesh overheads are taken from published service-mesh
|
||||
measurements~\cite{istio_overhead}; the comparison assumes co-scaled
|
||||
subsystems (\S\ref{sec:cost}).}
|
||||
\label{fig:cost}
|
||||
\end{figure*}
|
||||
Binary file not shown.
@@ -0,0 +1,469 @@
|
||||
\documentclass[11pt]{article}
|
||||
\usepackage{../shared/hanzocover}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage{amsmath,amssymb}
|
||||
\usepackage{graphicx}
|
||||
\usepackage{booktabs}
|
||||
\usepackage{xcolor}
|
||||
\usepackage{listings}
|
||||
\input{../shared/lstlang}
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{shapes,arrows.meta,positioning,fit,backgrounds,calc}
|
||||
\usepackage[hidelinks]{hyperref}
|
||||
|
||||
\definecolor{codegreen}{rgb}{0,0.6,0}
|
||||
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
|
||||
\definecolor{codepurple}{rgb}{0.58,0,0.82}
|
||||
\definecolor{backcolour}{rgb}{0.96,0.96,0.94}
|
||||
\definecolor{hzblue}{rgb}{0.12,0.30,0.55}
|
||||
\definecolor{hzslate}{rgb}{0.22,0.25,0.30}
|
||||
\definecolor{hzgreen}{rgb}{0.10,0.45,0.25}
|
||||
\definecolor{hzred}{rgb}{0.60,0.16,0.16}
|
||||
|
||||
\lstdefinestyle{mystyle}{
|
||||
backgroundcolor=\color{backcolour},
|
||||
commentstyle=\color{codegreen},
|
||||
keywordstyle=\color{codepurple},
|
||||
numberstyle=\tiny\color{codegray},
|
||||
stringstyle=\color{codegreen},
|
||||
basicstyle=\ttfamily\footnotesize,
|
||||
breakatwhitespace=false,
|
||||
breaklines=true,
|
||||
captionpos=b,
|
||||
keepspaces=true,
|
||||
numbers=none,
|
||||
showspaces=false,
|
||||
showstringspaces=false,
|
||||
showtabs=false,
|
||||
tabsize=2,
|
||||
frame=single,
|
||||
rulecolor=\color{codegray}
|
||||
}
|
||||
\lstset{style=mystyle}
|
||||
|
||||
\title{The Unified Cloud Binary: Collapsing a Microservices Control Plane into One Go Process}
|
||||
\author{Zach Kelling\thanks{Corresponding author: [email protected]} \\
|
||||
\textit{Hanzo Industries Inc (Techstars '17)} \\
|
||||
Los Angeles, California \\
|
||||
\texttt{https://hanzo.ai}}
|
||||
\date{HIP-0106 \quad Revision~1}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\hanzocoverpage
|
||||
|
||||
\twocolumn[
|
||||
\begin{@twocolumnfalse}
|
||||
\maketitle
|
||||
\begin{abstract}
|
||||
\noindent
|
||||
We describe the Hanzo \emph{unified cloud binary} (\texttt{hanzoai/cloud},
|
||||
HIP-0106): a single statically linked Go executable that mounts every
|
||||
platform subsystem---identity, key management, the Base application
|
||||
runtime, the API gateway, model inference, commerce, a virtual file
|
||||
system, message queues, DNS, and observability---into one operating-system
|
||||
process. The web substrate is \texttt{hanzoai/zip}, a framework built on
|
||||
Fiber~v3 that embeds (i)~\texttt{goja}, a pure-Go ECMAScript engine, for
|
||||
in-process evaluation of JavaScript handlers; (ii)~the \texttt{esbuild}
|
||||
Go API for transpiling TypeScript and modern JavaScript to ES5 at build
|
||||
or load time; and (iii)~ZAP, a capability RPC with promise pipelining,
|
||||
exposed over HTTP and WebSocket to external clients and reused as the
|
||||
in-process dispatch seam through a canonical \texttt{zaprpc.Registry}.
|
||||
The architecture lets an existing TypeScript backend port in unchanged
|
||||
by transpiling to ES5 and running inside the embedded engine, then be
|
||||
rewritten as native Go in place with no change to the HTTP or RPC
|
||||
contract. Within the process, calls between subsystems are Go function
|
||||
invocations rather than network round-trips, eliminating the service
|
||||
mesh, sidecars, per-call mutual TLS, and the container-per-service tax.
|
||||
The same artifact serves \texttt{api.hanzo.ai}, \texttt{api.lux.cloud},
|
||||
\texttt{api.zoo.cloud}, and every white-label tenant; brand and the set
|
||||
of enabled subsystems are deployment configuration. We give the
|
||||
architecture, a migration path validated against an in-flight
|
||||
tRPC-to-ZAP port, a sharp boundary between in-process evaluation and the
|
||||
separate sandboxed \texttt{hanzoai/runtime}, and a cost model grounded
|
||||
in published service-mesh and microservices-overhead measurements.
|
||||
\end{abstract}
|
||||
\vspace{1.5em}
|
||||
\end{@twocolumnfalse}
|
||||
]
|
||||
|
||||
\section{Introduction}
|
||||
|
||||
A control plane for a multi-tenant SaaS platform of this shape---identity,
|
||||
keys, an application runtime, a gateway, inference, commerce, storage,
|
||||
messaging---is conventionally decomposed into a constellation of
|
||||
independently deployed services. Each service runs in its own container,
|
||||
is fronted by a sidecar proxy, communicates with its peers over the
|
||||
network, and is wired together by a service mesh that terminates mutual
|
||||
TLS and enforces policy on every hop. This decomposition is the default
|
||||
not because the workload demands it but because the tooling makes it the
|
||||
path of least resistance~\cite{fowler_microservices}.
|
||||
|
||||
For a platform whose subsystems are co-released, co-owned, and almost
|
||||
always co-located on the request path, this default is a poor fit. A login
|
||||
that touches identity, then keys, then the gateway pays three network
|
||||
round-trips, three serialization boundaries, and three sidecar traversals
|
||||
to accomplish what is, logically, three function calls. The compute spent
|
||||
on proxies and (de)serialization is pure overhead; the latency is a
|
||||
stairstep that grows with the number of hops; and the deploy graph couples
|
||||
teams who must coordinate version skew across a distributed system they did
|
||||
not need to distribute.
|
||||
|
||||
This paper describes the alternative Hanzo has converged on: a single Go
|
||||
binary that mounts every subsystem into one process. We make four claims.
|
||||
First, for co-located subsystems the unified binary removes the dominant
|
||||
sources of microservices overhead---inter-service serialization, the
|
||||
sidecar data path, and per-call mutual TLS---by turning network RPC into
|
||||
function calls (\S\ref{sec:arch}, \S\ref{sec:perf}). Second, the framework
|
||||
beneath the binary, \texttt{zip}, admits existing TypeScript backends
|
||||
without a rewrite by transpiling them to ES5 and evaluating them in an
|
||||
embedded pure-Go engine, then permits an incremental, contract-preserving
|
||||
rewrite to native Go (\S\ref{sec:migration}). Third, the same artifact
|
||||
serves many brands and tenants because brand and the enabled subsystem set
|
||||
are configuration, not code (\S\ref{sec:multitenancy}). Fourth, there is a
|
||||
principled line between in-process evaluation of \emph{trusted} code and
|
||||
the separate sandboxed runtime for \emph{untrusted} user code
|
||||
(\S\ref{sec:boundary}). We close with a cost model
|
||||
(\S\ref{sec:cost}) anchored in published measurements rather than
|
||||
synthetic numbers.
|
||||
|
||||
\section{Motivation}
|
||||
\label{sec:motivation}
|
||||
|
||||
\paragraph{Compute spent on plumbing, not work.}
|
||||
A service mesh inserts a proxy on both ends of every call. Measured
|
||||
overheads are not negligible: a systematic study of Istio reports added
|
||||
per-request latency on the order of single-digit milliseconds and
|
||||
substantial CPU consumption attributable to the proxy data path, dominated
|
||||
by connection handling, serialization, and policy
|
||||
evaluation~\cite{istio_overhead}. When the logical operation is a function
|
||||
call's worth of work, a millisecond of proxy latency and a proxy-sized CPU
|
||||
slice per hop is the bulk of the cost.
|
||||
|
||||
\paragraph{Latency is a stairstep in the hop count.}
|
||||
Microservice request fan-out turns a single user action into a tree of
|
||||
internal calls. The DeathStarBench study shows that end-to-end tail latency
|
||||
in microservice topologies is governed by the depth and width of this
|
||||
call tree, and that queuing and serialization at each tier
|
||||
compound~\cite{deathstarbench}. Each additional network boundary on the
|
||||
critical path adds a serialization round trip and a scheduling boundary;
|
||||
none of that work advances the computation.
|
||||
|
||||
\paragraph{The distributed-systems tax is involuntary.}
|
||||
Splitting an in-process call into a network call replaces a deterministic
|
||||
function invocation with something that can partially fail, time out, or
|
||||
reorder---the classic objection that RPC papers over a fundamental
|
||||
semantic gap between local and remote calls~\cite{rpc_fallacy}. A platform
|
||||
that distributes subsystems it could have co-located pays this tax---retry
|
||||
logic, idempotency keys, circuit breakers, distributed tracing to
|
||||
reconstruct a call tree---without the scaling benefit that would justify
|
||||
it.
|
||||
|
||||
\paragraph{Deploy coupling without isolation benefit.}
|
||||
Co-released subsystems gain little operational isolation from separate
|
||||
deployment: a breaking change in the identity contract still requires the
|
||||
gateway to be updated in lockstep. What separate deployment does add is
|
||||
version-skew surface, N pipelines, and N rollouts to coordinate. Amazon
|
||||
Prime Video's widely cited account of consolidating a distributed
|
||||
media-monitoring pipeline back into a single process---reporting roughly an
|
||||
order-of-magnitude cost reduction for that workload---is a concrete data
|
||||
point that for some shapes the monolith is the
|
||||
correct engineering choice~\cite{amazon_prime_monolith}. We do not claim
|
||||
this generalizes to all systems; we claim it applies to a co-located
|
||||
control plane, and we make the conditions explicit.
|
||||
|
||||
\paragraph{What we are \emph{not} claiming.}
|
||||
The unified binary is not a rejection of horizontal scaling: the process is
|
||||
stateless at the request layer and runs as many replicas as load demands,
|
||||
one per region or per cell. The claim is narrower and sharper:
|
||||
\emph{subsystems that sit on the same request path and ship together should
|
||||
share an address space.} Genuinely independent, separately scaled, or
|
||||
untrusted components stay out of the binary---most importantly the
|
||||
sandboxed code-execution runtime (\S\ref{sec:boundary}).
|
||||
|
||||
\section{Architecture}
|
||||
\label{sec:arch}
|
||||
|
||||
\subsection{The layered binary}
|
||||
|
||||
Figure~\ref{fig:cake} shows the request path. HTTP and WebSocket clients
|
||||
reach a Fiber~v3 router~\cite{fiber}. The router dispatches to \texttt{zip}
|
||||
handlers. A handler resolves to one of two execution strategies: a
|
||||
\emph{native Go} subsystem call, or evaluation of a \emph{transpiled
|
||||
JavaScript} handler inside the embedded \texttt{goja}
|
||||
engine~\cite{goja}. Both strategies terminate in the same subsystem APIs;
|
||||
the choice is invisible to the client and to peer subsystems.
|
||||
|
||||
\input{figures/architecture}
|
||||
|
||||
\texttt{zip} is a Go module (\texttt{github.com/hanzoai/zip}) that
|
||||
wraps Fiber with the seams the platform needs: a module system
|
||||
(\texttt{module.go}) for mounting subsystems, lifecycle and shutdown
|
||||
ordering (\texttt{closers.go}), an OpenAPI surface
|
||||
(\texttt{openapi.go}) generated from the same specs that drive the
|
||||
\texttt{hanzoai/openapi} registry, and---centrally---\texttt{zaprpc.Registry},
|
||||
the one place where a method name resolves to a handler. The registry is
|
||||
the dispatch seam shared by the in-process and over-the-wire paths.
|
||||
|
||||
\subsection{ZAP: one RPC, two domains}
|
||||
|
||||
ZAP is a capability RPC modeled on Cap'n Proto's promise
|
||||
pipelining~\cite{capnproto}: a call may be issued against the
|
||||
\emph{result} of an earlier, still-pending call, so a chain of dependent
|
||||
operations collapses into a single round trip rather than one per step.
|
||||
ZAP serves two domains from one definition.
|
||||
|
||||
\emph{Over the wire}, external clients speak ZAP over HTTP (request/response)
|
||||
or WebSocket (bidirectional, for streaming and pipelined chains). This is
|
||||
the public contract: an SDK, a browser, or a peer cluster sees a stable
|
||||
method surface.
|
||||
|
||||
\emph{Inside the process}, the same \texttt{zaprpc.Registry} resolves a
|
||||
method name directly to a Go function. A subsystem that needs identity does
|
||||
not open a socket; it calls into the identity module through the registry,
|
||||
and the call is an ordinary function invocation with no serialization. The
|
||||
registry is the decomplecting seam: \emph{policy} (which method is exposed,
|
||||
to whom) is separated from \emph{transport} (wire vs.\ in-process), and
|
||||
both are separated from \emph{implementation} (Go function vs.\ goja
|
||||
handler).
|
||||
|
||||
\subsection{esbuild as the build-time and load-time transpiler}
|
||||
|
||||
TypeScript and modern JavaScript do not run in \texttt{goja} directly;
|
||||
\texttt{goja} targets ES5.1 with select later features. \texttt{zip}
|
||||
embeds the \texttt{esbuild} Go API~\cite{esbuild} to transpile and bundle
|
||||
handler sources to ES5. esbuild runs in-process (it is itself a Go
|
||||
program), so transpilation needs no external toolchain and no CGO. Two
|
||||
modes are supported: \emph{build-time}, where handlers are transpiled once
|
||||
and embedded into the binary via \texttt{go:embed}; and \emph{load-time},
|
||||
where a handler bundle is transpiled on first use and cached. Build-time is
|
||||
the default for shipped code; load-time exists for development and for the
|
||||
migration window (\S\ref{sec:migration}).
|
||||
|
||||
\subsection{No CGO, one artifact}
|
||||
|
||||
Both \texttt{goja} and the \texttt{esbuild} Go API are pure Go. The binary
|
||||
therefore links statically with \texttt{CGO\_ENABLED=0} and cross-compiles
|
||||
to every target with the standard Go toolchain---no native toolchain per
|
||||
architecture, no QEMU emulation step. The same source produces an
|
||||
\texttt{amd64} and an \texttt{arm64} artifact in parallel on native
|
||||
runners. One artifact, parameterized by configuration, is the unit of
|
||||
deployment for every brand and tenant.
|
||||
|
||||
\section{Migration Path}
|
||||
\label{sec:migration}
|
||||
|
||||
The framework's purpose is to make migration \emph{incremental} and
|
||||
\emph{contract-preserving}. An existing TypeScript service ports in three
|
||||
stages, and the HTTP/ZAP contract is invariant across all three.
|
||||
|
||||
\paragraph{Stage 0 --- lift.}
|
||||
The service's handlers are transpiled to ES5 with esbuild and registered in
|
||||
\texttt{zaprpc.Registry} as goja-evaluated handlers. The service now runs
|
||||
inside the unified binary. No handler logic changed; the wire contract is
|
||||
identical. Inter-subsystem calls that previously crossed the network (e.g.\
|
||||
a fetch to the identity service) are rewritten at the registry boundary to
|
||||
resolve in-process, which is the only change a lift requires and is purely
|
||||
additive at the seam.
|
||||
|
||||
\paragraph{Stage 1 --- strangle.}
|
||||
Hot or contract-critical handlers are rewritten as native Go and swapped in
|
||||
at the registry. Because the registry resolves a method name to whichever
|
||||
implementation is bound, a Go handler and a goja handler are
|
||||
interchangeable behind the same name. Rewriting proceeds one method at a
|
||||
time with no flag day; at every point the binary is shippable.
|
||||
|
||||
\paragraph{Stage 2 --- retire.}
|
||||
When the last method behind a service is native Go, the goja path and the
|
||||
transpiled bundle for that service are dropped. The esbuild dependency
|
||||
remains only where JavaScript handlers remain.
|
||||
|
||||
\paragraph{Case study: tRPC to ZAP.}
|
||||
The \texttt{hanzoai/esign} service is mid-migration from
|
||||
tRPC~\cite{trpc} to ZAP. tRPC's typed router maps cleanly onto the ZAP
|
||||
method surface: a tRPC procedure becomes a ZAP method, and the
|
||||
end-to-end type safety tRPC provides on the wire is preserved by ZAP's
|
||||
schema. In the most recent round, all fourteen of fourteen routers are
|
||||
served server-side through the ZAP surface (14/14), with the client
|
||||
SDK retargeted from the tRPC link to the ZAP transport. The contract seen
|
||||
by the frontend did not change; the transport beneath it did. This is the
|
||||
strangle stage in practice: the routers were ported behind a stable
|
||||
method surface without a coordinated client/server cutover.
|
||||
|
||||
\section{Multi-Tenancy}
|
||||
\label{sec:multitenancy}
|
||||
|
||||
The same artifact serves \texttt{api.hanzo.ai}, \texttt{api.osage.cloud},
|
||||
\texttt{api.lux.cloud}, \texttt{api.zoo.cloud}, and every white-label
|
||||
tenant. Two things are configuration, not code: the \emph{brand} (logo,
|
||||
title, identity issuer, default domain), resolved per request from the
|
||||
hostname; and the \emph{enabled subsystem set}, resolved per deployment.
|
||||
A tenant that needs identity and commerce but not inference mounts only
|
||||
those modules; the binary is the same, the module graph differs.
|
||||
|
||||
This is the natural payoff of the module system and the registry. Mounting
|
||||
a subsystem is registering its methods; not mounting it is not registering
|
||||
them. Brand resolution is a middleware that reads the hostname and binds a
|
||||
brand context that subsystems consult for issuer, theme, and policy. Per
|
||||
HIP-0106, brand isolation is enforced at the data layer---every query is
|
||||
scoped to the tenant resolved from the authenticated identity---so a single
|
||||
process serving many brands never crosses tenant boundaries. The
|
||||
white-label separation rules (no cross-brand logos, hostname-based brand
|
||||
detection) are realized as configuration consumed by one binary, not as
|
||||
forked deployments.
|
||||
|
||||
\section{The Code-Execution Boundary}
|
||||
\label{sec:boundary}
|
||||
|
||||
The most important design line in this work separates \emph{in-process
|
||||
evaluation of trusted code} from \emph{sandboxed execution of untrusted
|
||||
code}. Conflating them is the failure mode the boundary exists to prevent.
|
||||
|
||||
\paragraph{In-process goja: trusted, fast, same address space.}
|
||||
\texttt{goja} runs JavaScript in the binary's own address space with no
|
||||
isolation boundary. That is precisely why it is appropriate only for
|
||||
\emph{trusted} code: platform handlers authored by the platform team,
|
||||
transpiled from the platform's own TypeScript, and shipped in the binary.
|
||||
For this code, sharing the address space is the point---a goja handler
|
||||
calls a Go subsystem through the registry with no serialization, and the
|
||||
performance floor is a function call. goja is the right tool when the code
|
||||
is trusted and the call is on the hot path (HIP-0105, the in-process
|
||||
extension runtime).
|
||||
|
||||
\paragraph{\texttt{hanzoai/runtime}: untrusted, isolated, separate.}
|
||||
User-supplied code, agent-generated code, and computer-use workloads are
|
||||
\emph{untrusted} and must not share the control plane's address space. They
|
||||
run in \texttt{hanzoai/runtime}, a distinct platform that isolates
|
||||
execution in containers and microVMs with hardware-virtualization
|
||||
boundaries in the lineage of Firecracker~\cite{firecracker} and the
|
||||
isolate-per-tenant model of edge
|
||||
runtimes~\cite{cloudflareworkers,wasm_serverless}. \texttt{runtime} ships
|
||||
its own SDKs (Go, Python, TypeScript) and is out of scope for the control
|
||||
plane binary except as a downstream callee.
|
||||
|
||||
\paragraph{The rule.}
|
||||
Code the platform authors and trusts, on the hot path, runs in-process via
|
||||
goja. Code the platform receives and cannot trust runs in \texttt{runtime}
|
||||
behind a virtualization boundary. The two never swap roles. A handler is
|
||||
never promoted from \texttt{runtime} into goja, and untrusted input is
|
||||
never evaluated in goja. Stating the rule sharply is the contribution; the
|
||||
mechanisms on each side are conventional.
|
||||
|
||||
\section{Cost Model}
|
||||
\label{sec:cost}
|
||||
|
||||
We compare a representative decomposed control plane against the unified
|
||||
binary. The decomposed baseline is $N$ services, each in its own container
|
||||
with a sidecar proxy, joined by a service mesh; the unified case is one
|
||||
container per replica. We avoid fabricated benchmarks and parameterize the
|
||||
model by published overheads; the figures below are illustrative of the
|
||||
\emph{shape} of the saving, not a measurement of a specific deployment.
|
||||
|
||||
\input{figures/costsketch}
|
||||
|
||||
\paragraph{Container and proxy footprint.}
|
||||
The decomposed stack pays, per service, for a container runtime slice and a
|
||||
sidecar. With $N$ services and $R$ replicas, that is $N\cdot R$ application
|
||||
containers \emph{plus} $N\cdot R$ sidecars; the unified binary pays for
|
||||
$R$ containers and no sidecars. The sidecar CPU and memory reservation is
|
||||
a fixed tax per pod that the service-mesh literature measures in the
|
||||
hundreds of millicores and tens to low-hundreds of megabytes under
|
||||
load~\cite{istio_overhead}; multiplied by $N\cdot R$ it is the dominant
|
||||
avoidable cost.
|
||||
|
||||
\paragraph{Per-call overhead.}
|
||||
Let a user action touch $h$ subsystems on its critical path. The decomposed
|
||||
stack pays $h$ network hops, each with two proxy traversals and a
|
||||
serialize/deserialize pair; the unified binary pays $h$ function calls and
|
||||
zero serializations. With per-hop mesh overhead measured in the low
|
||||
single-digit milliseconds~\cite{istio_overhead}, a four-subsystem action
|
||||
sheds several milliseconds of pure overhead and the CPU that produced it.
|
||||
|
||||
\paragraph{Operational surface.}
|
||||
$N$ pipelines, $N$ rollouts, $N$ dashboards, and a mesh control plane
|
||||
collapse to one pipeline, one rollout, and one process to observe. This is
|
||||
not a compute cost but it is a real and recurring engineering cost; the
|
||||
DeathStarBench and service-mesh studies both note operational complexity as
|
||||
a first-order property of microservice topologies~\cite{deathstarbench,istio_overhead}.
|
||||
|
||||
\paragraph{What the unified binary does \emph{not} save.}
|
||||
It does not reduce the work the application actually performs, and it does
|
||||
not help when subsystems must scale independently---if inference needs ten
|
||||
times the replicas of identity, co-locating them wastes the smaller
|
||||
subsystem's headroom. The model assumes co-scaling, which holds for the
|
||||
control plane and fails for the inference data plane; the latter is
|
||||
deployed separately for exactly this reason.
|
||||
|
||||
\section{Performance}
|
||||
\label{sec:perf}
|
||||
|
||||
\paragraph{Function call vs.\ network RPC.}
|
||||
The structural performance argument is that an in-process subsystem call is
|
||||
a Go function invocation---nanoseconds, no allocation beyond arguments, no
|
||||
serialization---against a network RPC's microseconds-to-milliseconds of
|
||||
serialize, syscall, network, deserialize, and (with a mesh) two proxy
|
||||
traversals. The unified binary moves every co-located call from the latter
|
||||
class to the former. The latency floor of an intra-binary call is set by
|
||||
Go's call and scheduling cost, not by the network stack.
|
||||
|
||||
\paragraph{Promise pipelining at the edge.}
|
||||
For calls that \emph{do} cross the network---external clients, peer
|
||||
clusters---ZAP's promise pipelining~\cite{capnproto} collapses dependent
|
||||
call chains into a single round trip. A client that creates a resource and
|
||||
immediately operates on it issues the second call against the promise of
|
||||
the first; the server resolves the chain locally. This bounds edge latency
|
||||
by one round trip per dependent chain rather than one per call, which is
|
||||
the same economy the in-process path gets for free, recovered for the cases
|
||||
that must remain remote.
|
||||
|
||||
\paragraph{goja overhead, honestly.}
|
||||
A goja-evaluated handler is slower than the equivalent native Go handler:
|
||||
it interprets ECMAScript and bridges values across the Go/JS boundary. This
|
||||
is the cost of Stage~0 (\S\ref{sec:migration}) and the reason Stage~1
|
||||
exists. The design does not pretend goja is free; it makes the goja path a
|
||||
\emph{temporary} state that the strangle stage retires on the hot path
|
||||
while keeping the cold path in JavaScript as long as that is cheaper to
|
||||
maintain than to rewrite.
|
||||
|
||||
\section{Future Work}
|
||||
\label{sec:future}
|
||||
|
||||
Three directions extend the framework without changing its shape. First,
|
||||
\emph{additional in-process language runtimes}---a \texttt{pyvm} and a
|
||||
\texttt{v8vm} mounted behind the same registry seam---so trusted handlers
|
||||
in Python or via a V8 isolate dispatch identically to goja handlers, with
|
||||
the isolate option offering a stronger in-process boundary than goja for
|
||||
code that is trusted but warrants defense in depth. Second,
|
||||
\emph{hot reload} of goja modules: because load-time transpilation already
|
||||
exists, swapping a handler bundle without restarting the process is a
|
||||
registry rebinding, useful in development and for cold-path handlers.
|
||||
Third, \emph{gRPC-on-ZAP interop}~\cite{grpc}, exposing the ZAP surface to
|
||||
gRPC clients so existing gRPC ecosystems consume the platform without a new
|
||||
SDK, and \emph{deeper observability}, attributing power and CPU to
|
||||
in-process subsystems with the granularity that tools such as
|
||||
Kepler~\cite{kepler} bring to containers---harder inside one process,
|
||||
and the right place to invest the operational savings the unified binary
|
||||
returns.
|
||||
|
||||
\section{Conclusion}
|
||||
|
||||
For a co-located, co-released control plane, decomposition into networked
|
||||
microservices spends compute on proxies and serialization, adds latency in
|
||||
proportion to the hop count, and imposes a distributed-systems tax with no
|
||||
scaling benefit to justify it. The Hanzo unified cloud binary collapses the
|
||||
control plane into one Go process: \texttt{zip} over Fiber~v3, ZAP serving
|
||||
both the wire and the in-process dispatch seam, goja and esbuild admitting
|
||||
TypeScript backends without a rewrite, and a strangle path to native Go
|
||||
that never breaks the contract. One artifact serves every brand and tenant
|
||||
by configuration. The boundary against untrusted code---in-process goja for
|
||||
trusted hot paths, the sandboxed \texttt{runtime} for everything else---is
|
||||
the load-bearing design decision, and we have stated it sharply. The saving
|
||||
is not magic; it is the removal of plumbing that the workload never needed.
|
||||
|
||||
{\small
|
||||
\bibliographystyle{plain}
|
||||
\bibliography{references}
|
||||
}
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,164 @@
|
||||
@misc{hip0106,
|
||||
author = {{Hanzo Industries}},
|
||||
title = {{HIP-0106: Unified Cloud Binary}},
|
||||
howpublished = {Hanzo Improvement Proposal},
|
||||
year = {2026},
|
||||
note = {\url{https://github.com/hanzoai/hips}}
|
||||
}
|
||||
|
||||
@misc{hip0105,
|
||||
author = {{Hanzo Industries}},
|
||||
title = {{HIP-0105: In-Process Extension Runtime}},
|
||||
howpublished = {Hanzo Improvement Proposal},
|
||||
year = {2026},
|
||||
note = {\url{https://github.com/hanzoai/hips}}
|
||||
}
|
||||
|
||||
@misc{zip,
|
||||
author = {{Hanzo Industries}},
|
||||
title = {{zip: A Fiber-based Web Framework with Embedded ZAP RPC}},
|
||||
howpublished = {\url{https://github.com/hanzoai/zip}},
|
||||
year = {2026}
|
||||
}
|
||||
|
||||
@misc{cloud,
|
||||
author = {{Hanzo Industries}},
|
||||
title = {{cloud: Unified Go Control Plane and Binary for the Hanzo Platform}},
|
||||
howpublished = {\url{https://github.com/hanzoai/cloud}},
|
||||
year = {2026}
|
||||
}
|
||||
|
||||
@misc{fiber,
|
||||
author = {{Fiber Authors}},
|
||||
title = {{Fiber: An Express-inspired Web Framework for Go}},
|
||||
howpublished = {\url{https://github.com/gofiber/fiber}},
|
||||
year = {2025}
|
||||
}
|
||||
|
||||
@misc{goja,
|
||||
author = {Dmitry Panov and {goja contributors}},
|
||||
title = {{goja: ECMAScript/JavaScript Engine in Pure Go}},
|
||||
howpublished = {\url{https://github.com/dop251/goja}},
|
||||
year = {2025}
|
||||
}
|
||||
|
||||
@misc{esbuild,
|
||||
author = {Evan Wallace},
|
||||
title = {{esbuild: An Extremely Fast Bundler for the Web}},
|
||||
howpublished = {\url{https://github.com/evanw/esbuild}},
|
||||
year = {2025}
|
||||
}
|
||||
|
||||
@misc{capnproto,
|
||||
author = {Kenton Varda},
|
||||
title = {{Cap'n Proto: Serialization and RPC with Promise Pipelining}},
|
||||
howpublished = {\url{https://capnproto.org}},
|
||||
year = {2013}
|
||||
}
|
||||
|
||||
@inproceedings{sandstorm,
|
||||
author = {Kenton Varda},
|
||||
title = {{Sandstorm: An Open-Source Platform for Personal and Private Cloud Servers}},
|
||||
booktitle = {Sandstorm Technical Documentation},
|
||||
year = {2014},
|
||||
note = {\url{https://sandstorm.io}}
|
||||
}
|
||||
|
||||
@misc{cloudflareworkers,
|
||||
author = {Kenton Varda},
|
||||
title = {{Fine-Grained Sandboxing with V8 Isolates}},
|
||||
howpublished = {Cloudflare Blog},
|
||||
year = {2018},
|
||||
note = {\url{https://blog.cloudflare.com/cloud-computing-without-containers/}}
|
||||
}
|
||||
|
||||
@inproceedings{firecracker,
|
||||
author = {Alexandru Agache and Marc Brooker and Alexandra Iordache and
|
||||
Anthony Liguori and Rolf Neugebauer and Phil Piwonka and
|
||||
Diana-Maria Popa},
|
||||
title = {{Firecracker: Lightweight Virtualization for Serverless Applications}},
|
||||
booktitle = {17th USENIX Symposium on Networked Systems Design and Implementation (NSDI 20)},
|
||||
year = {2020},
|
||||
pages = {419--434},
|
||||
publisher = {USENIX Association}
|
||||
}
|
||||
|
||||
@inproceedings{istio_overhead,
|
||||
author = {Xiangfeng Zhu and Guozhen She and Bowen Xue and Yu Zhang and
|
||||
Yongsu Zhang and Xuan Kelvin Zou and Xiongchun Duan and
|
||||
Peng He and Arvind Krishnamurthy and Matthew Lentz and
|
||||
Danyang Zhuo and Ratul Mahajan},
|
||||
title = {{Dissecting Service Mesh Overheads}},
|
||||
booktitle = {arXiv preprint arXiv:2207.00592},
|
||||
year = {2022}
|
||||
}
|
||||
|
||||
@inproceedings{deathstarbench,
|
||||
author = {Yu Gan and Yanqi Zhang and Dailun Cheng and Ankitha Shetty and
|
||||
Priyal Rathi and Nayan Katarki and others},
|
||||
title = {{An Open-Source Benchmark Suite for Microservices and Their
|
||||
Hardware-Software Implications for Cloud and Edge Systems}},
|
||||
booktitle = {Proceedings of the 24th International Conference on Architectural
|
||||
Support for Programming Languages and Operating Systems (ASPLOS)},
|
||||
year = {2019},
|
||||
pages = {3--18},
|
||||
publisher = {ACM}
|
||||
}
|
||||
|
||||
@article{rpc_fallacy,
|
||||
author = {Steve Vinoski},
|
||||
title = {{Convenience over Correctness}},
|
||||
journal = {IEEE Internet Computing},
|
||||
volume = {12},
|
||||
number = {4},
|
||||
pages = {89--92},
|
||||
year = {2008},
|
||||
publisher = {IEEE}
|
||||
}
|
||||
|
||||
@misc{amazon_prime_monolith,
|
||||
author = {{Prime Video Tech}},
|
||||
title = {{Scaling up the Prime Video Audio/Video Monitoring Service and
|
||||
Reducing Costs by 90\%}},
|
||||
howpublished = {Amazon Prime Video Engineering Blog},
|
||||
year = {2023}
|
||||
}
|
||||
|
||||
@book{fowler_microservices,
|
||||
author = {Sam Newman},
|
||||
title = {{Building Microservices: Designing Fine-Grained Systems}},
|
||||
edition = {2nd},
|
||||
publisher = {O'Reilly Media},
|
||||
year = {2021}
|
||||
}
|
||||
|
||||
@misc{grpc,
|
||||
author = {{gRPC Authors}},
|
||||
title = {{gRPC: A High Performance, Open Source Universal RPC Framework}},
|
||||
howpublished = {\url{https://grpc.io}},
|
||||
year = {2025}
|
||||
}
|
||||
|
||||
@misc{trpc,
|
||||
author = {Alex Johansson and {tRPC contributors}},
|
||||
title = {{tRPC: End-to-End Typesafe APIs Made Easy}},
|
||||
howpublished = {\url{https://trpc.io}},
|
||||
year = {2025}
|
||||
}
|
||||
|
||||
@misc{kepler,
|
||||
author = {{Cloud Native Computing Foundation}},
|
||||
title = {{Kepler: Kubernetes-based Efficient Power Level Exporter}},
|
||||
howpublished = {\url{https://sustainable-computing.io}},
|
||||
year = {2024}
|
||||
}
|
||||
|
||||
@inproceedings{wasm_serverless,
|
||||
author = {Phani Kishore Gadepalli and Sean McBride and Gregor Peach and
|
||||
Ludmila Cherkasova and Gabriel Parmer},
|
||||
title = {{Sledge: A Serverless-First, Light-Weight Wasm Runtime for the Edge}},
|
||||
booktitle = {Proceedings of the 21st International Middleware Conference},
|
||||
year = {2020},
|
||||
pages = {265--279},
|
||||
publisher = {ACM}
|
||||
}
|
||||
Reference in New Issue
Block a user