Привет, Хабр!

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

В статье рассмотрим два языка программирования, которые выделяются своим функциональным подходом и широким применением в микросервисной архитектуре: 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 для каждого элемента списка.


Статья подготовлена в преддверии старта курса Microservice Architecture. На странице курса вы можете бесплатно посмотреть записи прошедших вебинаров, а также подробно ознакомиться с программой.

Комментарии (1)


  1. dolfinus
    25.04.2024 20:54

    Акторная модель ведь фича не самой Scala, а Akka