Кратко про микросервисы на Scala и Erlang

Kate

Administrator
Команда форума
Микросервисы давно являются некой "попсой" для создания гибких, масштабируемых и отказоустойчивых систем. И естественное имеет свою реализацию в функциональном программирование.

В статье рассмотрим два языка программирования, которые выделяются своим функциональным подходом и широким применением в микросервисной архитектуре: Scala и Erlang.

Scala​

Scala - это достаточной мощный ЯП, который сочетает в себе функциональные и объектно-ориентированные подходы.

Одной из ключевых фич Scala заключается в его акторной модели, которая (очевидно) основана на концепции акторов. Акторы в Scala представляют собой небольшие вычислительные сущности, которые общаются друг с другом посредством отправки и получения сообщений. Такой подход к параллельному и распределенному программированию обеспечивает высокую степень отказоустойчивости и масштабируемости, что для микросервисов, как мы знаем - очень важно.

ФП в Scala поддерживается мощными функциональными конструкциями, такими как функции высшего порядка, неизменяемые структуры данных и паттерн матчинга.

Кроме того, Scala обладает большой экосистемой библиотек и фреймворков, спецом разработанных для создания микросервисов. Например, Akka — это популярный фреймворк, основанный на акторной модели Scala.

Еще важно отметить, что Scala также имеет преимущества в интеграции с существующим Java-кодом, что делает его привлекательным выбором для компаний, уже имеющих код на Java.

Пример создания микросервиса на Scala​

Прежде всего, установим Scala и необходимые зависимости. Также воспользуемся Akka HTTP для обработки HTTP запросов:

// build.sbt
name := "microservice-example"
version := "1.0"
scalaVersion := "2.13.8"

libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-actor-typed" % "2.6.17",
"com.typesafe.akka" %% "akka-stream" % "2.6.17",
"com.typesafe.akka" %% "akka-http" % "10.2.8",
"com.typesafe.akka" %% "akka-http-spray-json" % "10.2.8"
)
Создадим актор для обработки запросов:

import akka.actor.typed.ActorSystem
import akka.actor.typed.scaladsl.Behaviors

object Main extends App {
val system = ActorSystem(Behaviors.empty, "microservice")

// define actors and behavior here
}
Создадим простой маршрут для API:

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route

object Routes {
def helloRoute: Route =
path("hello") {
get {
complete("Hello, Habr!")
}
}
}
Настроим HTTP сервер с использованием Akka HTTP:

import akka.http.scaladsl.Http
import akka.actor.typed.scaladsl.adapter._

object Main extends App {
val system = ActorSystem(Behaviors.empty, "microservice")

val routes = Routes.helloRouter

Http().bindAndHandle(routes, "localhost", 8080)(system.toClassic)
}
Запускаем:

object Main extends App {
val system = ActorSystem(Behaviors.empty, "microservice")

val routes = Routes.helloRoute // Add more routes as needed

Http().bindAndHandle(routes, "localhost", 8080)(system.toClassic)

println("Server online at http://localhost:8080/")
}
Это базовый пример.

Scala также позволяет легко выполнять асинхронные HTTP запросы к внешним сервисам или микросервисам. Пример использования Akka HTTP Client для отправки GET запроса:

import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.model._
import scala.concurrent.Future
import scala.util.{Success, Failure}
import akka.stream.ActorMaterializer
import scala.concurrent.ExecutionContext.Implicits.global

object HttpClientExample {
def main(args: Array[String]): Unit = {
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()

val responseFuture: Future[HttpResponse] = Http().singleRequest(HttpRequest(uri = "http://example.com"))

responseFuture.onComplete {
case Success(response) =>
println(s"Request successful: $response")
// handle response
case Failure(ex) =>
println(s"Request failed: $ex")
}
}
}
Scala позволяет создавать гибкие маршруты для обработки HTTP запросов с помощью Akka HTTP. Пример маршрута, который принимает POST запросы на путь /api/data и фильтрует запросы по определенным условиям:

import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.Route

object Routes {
def dataRoute: Route =
pathPrefix("api") {
path("data") {
post {
entity(as[String]) { requestData =>
// process the request data
complete("Data received: " + requestData)
}
}
}
}
}
Часто есть необходимость в управлении состоянием. Есть различные подходы к этой задаче, включая использование акторов для изоляции состояния. Пример:

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}

object StateManagement {
// define messages
sealed trait Command
case class UpdateState(data: String) extends Command
case class GetState(replyTo: ActorRef[String]) extends Command

// define actor behavior
def stateManager(state: String): Behavior[Command] =
Behaviors.receiveMessage {
case UpdateState(data) =>
stateManager(data)
case GetState(replyTo) =>
replyTo ! state
Behaviors.same
}

def main(args: Array[String]): Unit = {
val system = ActorSystem(stateManager("Initial state"), "state-manager")

val stateManagerRef = system
.unsafeUpcast[StateManagement.Command]
.narrow

stateManagerRef ! GetState(System.out)
}
}

Erlang​

Фича Erlang также как и в Scala заключается в его акторной модели. Также есть механизм supervision, который позволяет строить отказоустойчивые системы. Каждый процесс в Erlang имеет надзорный процесс, который отвечает за его состояние. В случае сбоя процесса, надзорный процесс автоматом перезапускает его

Также Erlang обладает возможностью хот-свопа кода. Т.е можно обновлять приложение в реал тайме без простоев. Новая версия кода может быть внедрена в работающую систему без перезапуска.

И самое главное - Erlang изначально разработан для построения распределенных систем.

Реализация​

GenServer является базой для создания микросервисов на Erlang. Он обеспечивает асинхронное взаимодействие и управление состоянием:

-module(example_service).
-behaviour(gen_server).

%% API
-export([start_link/0, get_state/1]).

%% Callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%% State
-record(state, {data = []}).

%% API functions
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

get_state(Pid) ->
gen_server:call(Pid, get_state).

%% Callback functions
init([]) ->
{ok, #state{}}.

handle_call(get_state, _From, State) ->
{reply, State#state.data, State}.

handle_cast(_Msg, State) ->
{noreply, State}.

handle_info(_Info, State) ->
{noreply, State}.

terminate(_Reason, _State) ->
ok.

code_change(_OldVsn, State, _Extra) ->
{ok, State}.
Supervisor обеспечивает отказоустойчивость и перезапуск микросервисов в случае сбоев:

-module(example_supervisor).

-behaviour(supervisor).

%% API
-export([start_link/0]).

%% Callbacks
-export([init/1]).

%% Init function
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).

%% Callback function
init([]) ->
{ok, {{one_for_one, 5, 10},
[{example_service, {example_service, start_link, []},
permanent, 5000, worker, [example_service]}]}}.
Развернем на кластере:

%% запуска микросервиса на удаленном узле
start_remote_service(Node) ->
{ok, _} = net_kernel:start([example@hostname]),
{ok, _} = rpc:call(Node, example_service, start_link, []).
Масштабирование может быть достигнуто путем запуска доп. экземпляров микросервисов на разных узлах кластера:

%% запуск дополнительных экземпляров микросервиса на разных узлах кластера
start_additional_instances(NumInstances, Node) ->
lists:foreach(fun(_) -> start_remote_service(Node) end, lists:seq(1, NumInstances)).
Определяем функцию start_additional_instances, которая принимает два аргумента: NumInstances, представляющий собой количество дополнительных экземпляров микросервиса, которые хотим запустить, и Node, представляющий собой имя узла кластера, на котором мы хотим развернуть эти экземпляры.

Затем используем функцию lists:seq/2, чтобы сгенерировать список чисел от 1 до NumInstances. Далее используем функцию lists:foreach/2, чтобы выполнить функцию start_remote_service для каждого элемента списка.

 
Сверху