Функциональное программирование + Kubernetes, развертывание сервера записи потокового видео

Clojure

Ван Лэй, технический директор WishLife, поделился функциональным программированием и развертыванием сервисов записи потокового видео с использованием Kubernetes на конференции RTC 2019. WishLife — это платформа, которая использует видео, чтобы помочь семьям решать вопросы семейного общения. Платформа предоставляет услуги видеозаписи. Записанные видео будут сохраняться в облаке, чтобы члены семьи могли их посетить и посмотреть. Он поделился техническими моментами и опытом внедрения языков функционального программирования Clojure, Kubernetes и развертывания сервиса видеозаписи.

Ниже приводится стенограмма выступления:

Здравствуйте, все, я Ван Лей, я являюсь пользователем Heavy Emacs, и теперь язык кода имеет Clojure, JavaScript и Java. Я также ранний участник GraphQLQL. Я начал, и я также разработал тысячи продуктов SaaS. Далее вы поделитесь некоторыми мышлениями о моей работе.

Помимо технологий, в жизни я люблю серфинг и катание на лыжах. Я нашел проблему, у меня так много вещей, в которые я хочу поиграть, но так мало времени для себя. Во время игры мне часто звонят по работе. Поэтому мне интересно, есть ли способ сделать продукт менее проблемным, или даже если проблема есть, она минимальна.

С вопросами я нашел ответ, один — функциональное программирование Clojure, другой — Kubernetes. Преимущество функционального программирования в том, что оно может повысить эффективность моей работы в 10 раз, а тот же набор кода функциональное программирование будет писать быстрее. А Kubernetes позволяет нам преобразовывать предыдущий опыт развертывания сервера в код, делая его более работоспособным и делая работу по эксплуатации и обслуживанию воспроизводимой.

Как функциональное программирование может быть в 10 раз продуктивнее

Обычно мы используем три основные структуры командного программирования: последовательность, выбор и цикл. Функциональное программирование, с другой стороны, рассматривает компьютерные операции как вычисления функций. С точки зрения формул, это f (x) -> y. Код, который мы хотим написать, представляет собой функцию f, которая получает вход x и выводит y. программирование Код, который нужно написать, представляет собой эту функцию f, а преобразование в инженерный язык — «для определенного фиксированного ввода необходимо дать вывод». Мы также пишем подобный код в императивном программировании, а функциональное программирование доводит его до другой крайности, когда все делается как функция, независимо от порядка. Это заставляет думать совершенно по-другому. В функциональном программировании входной параметр может быть функцией, и выходной результат также может быть функцией.

Каковы особенности функционального программирования?

В функциональном программировании ваш код — это данные. Пишем код пишем и у большего количества людей будет такое ощущение: пишешь код сегодня, как бы круто ни было время писать свой, смотри через полгода, будешь думать, что это мусорный код. Но когда вы можете стать кодом данных, у кода будет жизнь, он может постоянно меняться, может постоянно меняющийся, развивающийся код — это код, который не мертв.

Многие из кода мы пишем, будь то в функциональном или императивном программировании, в конечном итоге призвано иметь побочные эффекты. Как понять эту проблему? Это эквивалентно написанию очень сложного алгоритма. Алгоритм должен в конечном итоге иметь вывод и оказывать влияние на внешний мир, например, оставляя запись в базе данных, написание файла или контроля некоторых внешних систем. Таким образом, мы Программа записи будет полезна.

Чистые функции, которые идут рука об руку с побочными эффектами. Если функция не имеет побочных эффектов, как она называется? Таким образом, в чистых функциях они дополняют друг друга. Если вы хотите, чтобы ваша программа была полезной, у вас должны быть побочные эффекты; если вы хотите сделать свою функцию многократно используемой, у вас не должно быть побочных эффектов.

Четвертая характеристика — неизменяемость данных. Это важная концепция. Эту концепцию можно увидеть во многих системах. Например, до Git традиционной системой управления версиями, которую мы использовали, была SVN, которая хранила копию файла и сохраняла изменение после каждого изменения Концептуально мы думали, что сохраняемые вещи должны быть очень маленькими, но правильными с точки зрения Git сохраняется копия каждого файла, а исходный файл не изменяется.Многие люди скажут, что размер файла, хранящегося в Git, должен быть намного больше, чем размер файла, хранящегося в SVN.На самом деле , наоборот, каждое изменение файла сохраняет файл, копии занимают меньше места. Таким же образом, это также преимущество неизменяемости.

В конце концов, все сводится к конкретному языку, который поддерживает функциональное программирование, вам не нужно учить новый язык, вы можете попробовать написать Java функциональным образом, хотя синтаксис, используемый при обдумывании проблем, все еще является JAVA, но это новый язык. Мой любимый язык функционального программирования называется Clojure, он основан на виртуальной машине Java, имеет динамическую типизацию, не имеет определений типов, а базовые структуры данных являются неизменяемыми структурами данных.

Как запустить код Clojure? Во-первых, это выполнение интерпретации, а за исполнением следует компиляция. Код Clojure в конечном итоге считывается и компилируется для генерации байт-кода Java. Написание кода JAVA и написание кода Clojure ничем не отличается от написания кода JAVA в действии. При написании кода на Clojure лучше использовать функцию REPL (цикл чтения-оценки-печати). Когда мы пишем код JAVA, сохранение кода занимает не менее 1 минуты, независимо от того, насколько быстры наши руки. Если вы пишете код Clojure, вы пишете код и нажимаете Enter, он выполнит результат и сообщит вам результат, процесс может занять всего 10 секунд. Вы избавляете себя от необходимости вручную компилировать и выполнять, Clojure сделает это за вас.

Я расскажу о двух основных синтаксисе Clojure. Я не хочу учить вас писать на Clojure, в основном это концепции. Код JAVA, который мы написали, выглядит следующим образом:

println("Hello World!")

Функциональное программирование выглядит так:

(println "Hello World!")

Поместите в скобки то, что должно быть Println, это список. Этот синтаксис прост, первый элемент — это функция, функция будет выполнена, и оператор вернет значение. Чтобы понять это с точки зрения функций, мы вызовем функцию Println, которая не имеет вывода и выводит на экран «Hello World!».

Как определить функцию?

(defn greet
  "Return a friendly greeting"
  [your-name]
  (str "Hello, " your-name))

Или через список, содержимое в круглых скобках — это все списки, вы можете думать о defn как о функции, приветствие — это имя ее функции, вторая строка — документ с ее описанием, третья строка — ее параметры, а последняя строка — The оператор, который он хочет выполнить. То есть определите функцию приветствия, которая будет выполнять оператор, написанный в последней строке кода. Например, если ввод «Том», вывод «Привет, Том».

Когда мы пишем код JAVA, средняя функция составляет не менее 20-50 строк кода. В функциональном программировании функция обычно состоит из трех или пяти строк, а максимум двадцать строк — это очень много.Двадцать строк кода могут превысить соответствующие 200 строк кода JAVA. (Пример программирования спикера на месте, подробную демонстрацию смотрите в «Видеообзоре» в конце статьи)

Apache Commons имеетisBlank. Давайте попробуем преобразовать его в код Clojure шаг за шагом.

  
public class StringUtils {
  public static boolean isBlank(String str) {
    int strLen;
    if (str == null || (strLen = str.length()) == 0) {
      return true;
    }
    for (int i = 0; i < strLen; i++) {
      if ((Character.isWhitespace(str.charAt(i)) == false)) {
        return false;
      }
}
    return true;
  }
}

Как мы уже говорили ранее, Clojure — язык с динамической типизацией и не имеет объявлений типов, поэтому мы сначала удаляем из него весь типизированный код, и в результате получается следующее.

public class StringUtils {
  public isBlank(str) {
    if (str == null || (strLen = str.length()) == 0) {
      return true;
    }
    for (int i = 0; i < strLen; i++) {
      if ((Character.isWhitespace(str.charAt(i)) == false)) {
        return false;
} }
    return true;
  }
}

Поскольку Clojure — это программа-функция, в ней нет понятия класса, поэтому мы должны удалить класс JAVA.

  
public isBlank(str) {
  if (str == null || (strLen = str.length()) == 0) {
    return true;
  }
  for (int i = 0; i < strLen; i++) {
    if ((Character.isWhitespace(str.charAt(i)) == false)) {
      return false;
    }
}
  return true;
}

В Clojure функции являются гражданами первого класса, поэтому вместо цикла for в предыдущем коде нужна только функция.

public isBlank(str) {
  if (str == null || (strlen = str.length() != 0) {
    return true;
  }
  every (ch in str) {
    Character.isWhitespace(ch);
}
  return true;
}

Затем мы хотим удалить нежелательные граничные условия.

public isBlan(str) {
  every (ch in str) {
    Character.isWhitespace(ch);
  }
}

Теперь давайте сравним, как должен выглядеть код Clojure:

(defn blank? [s]
  (every? (fn [c] (Character/isWhitespace c)) s))

Код JAVA, который оказался очень многословным, теперь превратился в предложение. Вот почему я говорю, что это помогло нам стать в 10 раз более продуктивными. Большинство функций уже есть, все, что вам нужно узнать, это то, что это за функции. map, reduce, filter, я думаю, что большинство проблем можно решить, сначала изучив эти три функции. Например, большая часть бизнес-кода, который мы пишем, в основном представляет собой операцию над набором данных.Эта операция может дать вам набор данных и позволить вам вернуть набор данных того же размера.Эта операция соответствует Clojure.map. Другой — предоставить вам набор данных и позволить вам вернуть меньший набор данных, чего можно добиться с помощью сокращения в Clojure. фильтр очень прост, это условная функция. На самом деле, многие денежные технологии основаны на этой простой идее.

После написания в Clojure сохраните его как пакет JAVA. Осталось запустить его в производство. Раньше нам нужно было учитывать, может ли машинная среда достичь рабочих условий, теперь это просто: через Docker мы можем запустить нашу программу на любой машине. Тем не менее, все еще есть проблема, контейнер Docker — это всего лишь экземпляр, как нам развертывать его в больших масштабах, в настоящее время нам нужно использовать Kubernetes.

Запись потокового развертывания видео службы с Kubernetes

Kubernetes — это платформа управления контейнерами производственного уровня, которая обеспечивает автоматическое развертывание, масштабирование и управление контейнерами. Таким образом, код, который мы написали ранее, становится образом Docker, а затем автоматизирует развертывание через Kubernetes.

В Kubernetes есть несколько основных концепций, которые нам необходимо понять:

  1. Cluster
  2. Node
  3. Pod
  4. Service
  5. IngressController
  6. PersistentVolume
  7. PersistentVolumeClaim

Давайте посмотрим на концептуальную карту. Это пользователь, обращающийся к балансировщику нагрузки в облаке. После передачи балансировщика нагрузки в кластер входной контроллер получит запрос пользователя, и запрос пользователя будет отправлен в службу, соответствующую развертыванию, и, наконец, в контейнер для выполнения, и окончательная обратная связь вернется таким же образом. Зная приведенные выше концепции, вы сможете развернуть любой продукт.

Способ развертывания — декларативное развертывание. Что означает декларативное развертывание? Раньше мы выполняли много команд во время развертывания. После выполнения каждой команды оно исчезает. У вас нет возможности сообщить другим о процессе развертывания. Методы этих операций нельзя передать следующему коллеге, который должен отвечать за эксплуатация и обслуживание. . В Kubernetes файл yaml используется для описания того, как развернуть. Kubernetes использует этот файл объявления, чтобы помочь вам развернуть. В нем записываются все ваши операции по развертыванию системы, и коллега, который примет на себя обязанности, будет знать, что делать, когда он видит файл.

Далее мы видим файл yaml. В соответствии с приведенной выше архитектурой мы должны начать описание от базового развертывания до входного контроллера.

Первым шагом является описание развертывания. Мы хотим развернуть сервер записи, адрес образа us.gcr.io/qatest-220319/recording-server, конкретный код выглядит следующим образом.

apiVersion: apps/v1beta1
kind: Deployment
metadata:
  labels:
    app: recording-server
  name: recording-server
spec:
  replicas: 1
  ...
  template:
...
    spec:
      containers:
      - image: us.gcr.io/qatest-220319/recording-server
        name: recording-server
        ports:
        - containerPort: 8080

Описание службы выглядит следующим образом, и в конечном итоге служба будет отправлена ​​​​на сервер записи.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: recording-service
  name: recording-service
spec:
  ports:
  - port: 8080
    name: high
    protocol: TCP
    targetPort: 8080
  selector:
    app: recording-server

Тогда есть Ingress Controller.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: recording-service-example-com
  annotations:
    kubernetes.io/ingress.class: "kong"
    kubernetes.io/tls-acme: "true"
    certmanager.k8s.io/cluster-issuer: letsencrypt-prod
spec: tls:
  - secretName: recording-service-example-com
    hosts:
    - recording.example.com
  rules:
  - host: recording.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: recording-service
  servicePort: 80
  

После выполнения предыдущего шага запускается служба без сохранения состояния. Здесь мы записываем видео, так что у нас получается много записей. Но теперь модуль не имеет состояния, после завершения работы модуля все записанные файлы будут потеряны. Итак, теперь нам нужен Volume для хранения этих записей. Поды исчезнут, а тома — нет. Ниже описан процесс определения тома.

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: recording-server
          image: us.gcr.io/qatest-220319/recording-server
          volumeMounts:
          - name: "recording-server-pv-storage"
            mountPath: /data
      volumes:
        - name: "recording-server-pv-storage"
          persistentVolumeClaim:
            claimName: "recording-server-pv-claim"

Однако мы не можем развернуть Volume вручную на каждом сервере, поэтому в настоящее время нам нужно управлять хранилищем записанных файлов через этот StatefulSet.

apiVersion: apps/v1
kind: StatefulSet
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: recording-server
          image: us.gcr.io/qatest-220319/recording-server
          volumeMounts:
          - name: "recording-server-pv-template"
            mountPath: /data
  volumeClaimTemplates:
    - metadata:
        name: recording-server-pv-template
      spec:
        resources:
          requests:
            storage: 128Gi
        accessModes: ["ReadWriteOnce"]
        

Наконец, нам также нужно добавить многопользовательское описание многопользовательской среды. Это достигается с помощью пространства имен.

apiVersion: v1
kind: Namespace
metadata:
  name: qa-recording

Наконец, чтобы легко определять, устанавливать и обновлять Kubernetes, мы можем представить Helm Charts. Мы можем выполнять эти операции с помощью шаблонов и инструментов командной строки, которые он предоставляет.


Наконец, прикрепитеАдрес загрузки PPT и видеообзора.