Reimagining the microservice: an early preview
source link: https://www.unison-lang.org/whats-new/unison-services-preview/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Apr 14, 2023
Reimagining the microservice: an early preview
We've developed a nicer way to build microservices in Unison. This post is a quick preview of what you can expect.
Building a microservice-powered backend today typically means taking on a lot of work that's not your application's business logic, and isn't very interesting or differentiated. For instance:
- Packaging a particular version of your service into a container image
- Deploying that container to some infrastructure where it can be run
- Managing communication between services, via JSON, HTTP, protocol buffers, and the like
- Dealing with serialization code at the boundary between the service and some durable storage layer
- Ensuring proper registration, discovery, and communication between services using appropriate service discovery patterns, tools, and platforms
- Handling failure and resiliency, for example in long-running workflows
Wouldn't it be nice if you could spend less time on these activities, and more time on the stuff that matters for your application?
A simple "Hello world" service
Let's have a look at a simple "Hello world" microservice (we'll just say "service" in the rest of this post), written and deployed toUnison Cloudwith a few lines of code:
hello : HttpRequest -> HttpResponse
hello req = HttpResponse.ok (Body (Text.toUtf8 "Hello, world!"))
deployHelloWorld = do cloud.services.http.deploy hello
The service's logic is just a regular function which accepts anHttpRequest
and returns anHttpResponse
.We also use regular Unison code to deploy the service.
Doingrun deployHelloWorld
in UCM will create and deploy the service:
scratch/main> run deployHelloWorld
Service deployed at https://3d03c8e7d82f1f4211e4d7762632c68.services.unison.cloud
Visit https://services.unison.cloud to see all your running services.
ServiceHash 0xs3d03c8e7d82f1f4211e4d7762632c68
There's no packaging step, no building containers, or anything like that. You just call a function in the Unison Cloud API to deploy the service. Unison automatically uploads your function and all of its dependencies to Unison Cloud, caching them on the server.
This service is now live on the internet. It's just like any other HTTP service and we can call it from the browser or from any other language. For instance, here's Python:
import requests
url = "https://3d03c8e7d82f1f4211e4d7762632c68.services.unison.cloud"
response = requests.get(url)
if response.status_code == 200:
print(response.text)
else:
print("Error: ", response.status_code)
Deployment of services takes mere seconds, and theUnison Cloudimplementation of services doesn't actually use any resources until the service is called. The platform will run and scale the service for you, and coming soon, services will even be JIT-compiled for speedy performance.
Updating a service
Let's make a trivial change to our service and redeploy it in seconds:
deployHelloWorld2 = do
logic req = HttpResponse.ok (Body (Text.toUtf8 "👋, world!"))
cloud.services.http.deploy logic
Notice that the URL is different:
scratch/main> run deployHelloWorld
Service deployed at https://0318cef3bf71ca76d9228e6d17f29bb21a.services.unison.cloud
Visit https://services.unison.cloud to see all your running services.
ServiceHash 0xs0318cef3bf71ca76d9228e6d17f29bb
By default, service URLs are based onServiceHash
,which is a hash of the service implementation. Using hashes as the identity of a service means that services are immutable and content-addressed: you don't modify a service, you instead deploy a new one.
We provide a separate layer for stably-named services whose implementations can evolve over time. (More on that in future posts)
Content-addressing isa recurring theme for Unisonand content-addressed services have some nice properties. For instance, service deployment is idempotent, and we can deploy as many versions of the "same" service as we like without these versions interfering and without needing to set up multiple staging environments.
Services can use effects
Services don't have to just be pure functions fromHttpRequest
toHttpResponse
.They can do lots of things, by leveraging Unison'sabilities:
- They can perform distributed computation using the
Remote
ability. Need to spawn a big map reduce job on the fly in response to a service request? Services can do that. - They can access secrets and configuration parameters specific to that service.
- They can issue HTTP requests.
- They can do logging, with logs consolidated and easily viewable in your Unison Cloud account.
- (coming soon) They can access durable and scalable Unison-native storage, with no serialization boilerplate to persist and unpersist values.
- And more...
Over time, we can easily grow the set of capabilities that services get access to.
For instance, let's add some logging to our "Hello World" service:
logic : HttpRequest ->{Log} HttpResponse
logic req =
log "Waving"
HttpResponse.ok (Body (Text.toUtf8 "👋, world!"))
deployHelloWorld3 = do cloud.services.http.deploy logic
Typed services and inter-service calls without the boilerplate
While HTTP is still the primary interface for most public-facing services, a lot of backends have services which are only called internally. It's unfortunate to have to pay the price of encoding / decoding boilerplate at each of these service call boundaries, and we can do better in Unison.
So far we've been dealing with HTTP services, which have the shapeHttpRequest ->{Remote} HttpResponse
,but Unison supports typed services where the input and output types can be anything we like. To deploy one of these "Unison-native" services, we use a more general functioncloud.services.deploy
(instead ofcloud.services.http.deploy
).
cloud.services.deploy : (a ->{Remote} b) ->{IO,Exception} ServiceHash a b
The argument to this function has the shapea ->{Remote} b
.deploy
returns aServiceHash a b
, wherea
is the input type of the service andb
is the output type. For example, a user lookup service might be represented by aServiceHash Username User
and in general your services can work with arbitrarily complex types. You can even have higher-order services that accept functions as arguments!
The big benefit here is that Unison-native service calls are typed, so there's no serialization code to write and you can never accidentally send the wrong type of data to a service. All you need to do is use:
Services.call : ServiceHash a b -> a ->{Services, Remote} b
For instance, here's an example of some service logic that finds a user's playlist by name and returns a list of the track titles for that playlist. There's no converting to and from JSON blobs or whatever else, you just call a function with a typed value and get back a typed reply:
serviceCallEx
: ServiceHash User [Playlist]
-> ServiceHash Track Track.Title
-> {Services, Remote} [Track.Title]
serviceCallEx playlists trackTitle =
Services.call playlists (User "alice")
|> List.filter (p -> Playlist.name p == "Discovery Weekly")
|> List.flatMap Playlist.tracks
|> Remote.parMap (Services.call trackTitle)
Notice that we're even collecting all the track titles for the playlist in parallel.
If while writing this code, we make a mistake and try to send a value of the wrong type to a service call, we get a type error at compile time, not a runtime error sometime after the service has been deployed.
An architecture for Unison-based backends
Though it's still early days, we suspect that a common pattern when building backends with Unison will be to create a collection of typed, Unison-native services that can all call each other easily, and then a public-facing gateway service that speaks HTTP and delegates to the Unison-native services.
It's this gateway service that will be called by the front-end or by other non-Unison services within your organization. (This gateway service may also be where common concerns such as authentication and authorization are dealt with.)
Here's an example — this code creates a single HTTP service with two routes, each of which delegates to a different typed microservice:
serviceCalls : ServiceHash User Nat
-> ServiceHash User Nat
-> {IO,Exception} ServiceHash HttpRequest HttpResponse
serviceCalls bumpUserCount accessUserCount =
publicFacing req =
getCount = do
Routes.get (root / "users" / "get-count")
user = parseUser (header "user")
n = Services.call accessUserCount user
HttpResponse.ok (Body (n |> Nat.toText |> toUtf8))
bumpCount = do
Routes.post (root / "users" / "bump-count")
user = parseUser (header "user")
n = Services.call bumpUserCount user
HttpResponse.ok (Body (n |> Nat.toText |> toUtf8))
Http.handler (getCount <|> bumpCount <|> 'notFound)
cloud.services.http.deploy publicFacing
Notice how within the public-facing service, we can call the other servicesbumpUserCount
andaccessUserCount
without any encoding and decoding boilerplate.
What's next?
Be on the lookout for future posts showing more of what's possible with this model, and posts with fully-worked example services that you can fork and use as templates for your own work (for instance "A simple Hello World Slackbot").
We'll be rolling out support for microservices on Unison Cloud in the coming months. If you're interested in getting access to the beta once it's available, sign up atunison.cloud.
Recommend
-
59
The Telegraph UK: Reimagining media with the help of Google Cloud 2019-02-06adminGoogleCloud
-
55
Journey to differentiation: Reimagining custome...
-
15
I recently decided to wrap up my time at Protocol Labs, and along with it, my time in open source research. I’ve spent the past 4+ years looking at how open source software is produced, from an economic and anthrop...
-
3
Reimagining Experimentation Analysis at NetflixToby Mao, Sri Sri Perangur,
-
7
Reimagining the Vendor-Only ModelToggle author information Posted on 08/12/2020 Reimagining the Vendor-Only ModelI have been trying to organize my personal thoughts on this and figure out how to carefuly articulate them fo...
-
11
Premium Home Chevron iconIt indicates an expandable section or menu, or sometimes previous / next navigation options....
-
5
Reimagining the AWS .NET deployment experience by Norm Johanson and Philip Pittle | on 17 MAR 2021 | in
-
7
Inside YouTube Reimagining video infrastructure to empower YouTube
-
4
...
-
4
Hipstamatic’s new social network looks like an Instagram throwback / Exclusively for iOS, the Hipstamatic photography app has relaunched as a social network platform that rebuilds what made Instagram so great.
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK