Distributed Phoenix Chat using Redis PubSub

原文 https://www.poeticoding.com/distributed-phoenix-chat-using-redis-pubsub/

这篇文章里 Create a High-Availability Kubernetes Cluster on AWS with Kops, 我们学到怎么创建Kubernetes集群来部署Phoenix Chat应用, 这在单节点上跑的很好, 当我们试图扩展时,我们看到消息没有发送到所有浏览器。

在下图中,我们看到一个配置,其中两个聊天服务器是隔离的。当负载均衡器将两个WebSocket连接路由到两个不同的服务器时,两个浏览器无法相互发送消息。

此问题通常使用外部组件(如Redis)来解决, 使用redis将消息广播到群集中的所有节点。

使用Elixir可以不依赖redis, 纯粹依赖Elixir不用节点见的通讯

在本文中,我们将看到如何横向扩展Phoenix Chat,利用Redis PubSub运行在多个服务器。

Redis PubSub

Github repo https://github.com/poeticoding/phoenix_chat_example/tree/pubsub_redis

什么是 PubSub messaging (发布/订阅消息收发)?

Publish/subscribe messaging, or pub/sub messaging, is a form of asynchronous service-to-service communication used in serverless and microservices architectures. In a pub/sub model, any message published to a topic is immediately received by all of the subscribers to the topic.

AWS – Pub/Sub Messaging

发布/订阅消息收发是一种异步的服务间通信方式,适用于无服务器和微服务架构。在发布/订阅模式下,发布到主题的任何消息都会立即被主题的所有订阅者接收。发布/订阅消息收发可用于启用事件驱动架构,或分离应用程序,以提高性能、可靠性和可扩展性。

为了更好地理解PubSub的工作原理,让我们在Redis实验下。运行一个redis服务器

$ docker run --name redis -d redis

执行redis-cli

$ docker exec -it redis redis-cli

在图中,运行了4个客户端, 3个客户端订阅了 my_channel

 Phoenix PubSub Redis adapter

在phoenix使用 redis pubsub功能,要用到 The Redis PubSub adapter for the Phoenix framework
这个包 https://github.com/phoenixframework/phoenix_pubsub_redis

集成很容易

#mix.exs
defp deps do
[
  ...
  {:phoenix_pubsub_redis, "~> 2.1"}
]
end
#config/config.exs
config :chat, Chat.Endpoint,
  url: [host: "localhost"],
  root: Path.expand("..", __DIR__),
  secret_key_base: ".......",
  debug_errors: false,
  pubsub: [
    name: Chat.PubSub,
    adapter: Phoenix.PubSub.Redis,
    host: "localhost", port: 6379,
    node_name: System.get_env("NODE")
 ]
 ```
 
 配置 redis host, 每个server要配置不贩node_name

 
启动

Terminal 1

$ NODE=n1 PORT=4000 mix phx.server

# Terminal 2
$ NODE=n2 PORT=4001 mix phx.server


2019/1/31 posted in  阅读笔记 Elixir

Create a High-Availability Kubernetes Cluster on AWS with Kops

https://www.poeticoding.com/create-a-high-availability-kubernetes-cluster-on-aws-with-kops/

AWS上轻松运行 Kubernetes 集群 部署 elixir 项目

使用 kops 部署 Kubernetes Operations (kops) - Production Grade K8s Installation, Upgrades, and Management

AWS 对kops有良好支持

部署此项目 https://github.com/poeticoding/phoenix_chat_example

High-Availability Cluster 高可用集群

结构图

要拥有HA集群,我们至少需要三个master服务器(管理整个Kubernetes集群的服务器)和两个worker服务器

AWS account and IAM role

配置账号

需要用到 aws-cli

$ aws configure

AWS Access Key ID: YOUR_ACCESS_KEY
AWS Secret Access Key: YOUR_SECRET_ACCESS_KEY
Default region name [None]:
Default output format [None]:

iam 用户需要 AdministratorAccess 权限

查看用户

$ aws iam list-users
{
    "Users": [
        {
            "Path": "/",
            "UserName": "alvise",
            "UserId": ...,
            "Arn": ...
        }
    ]
}

kops是在AWS上创建Kubernetes集群的工具 kubectl是我们用来管理集群启动运行

 Real domain in Route53

使用kops 需要配置域名

命令的使用等等

Kubernetes API and Security Group

k8s的api默认是公开的, 需要配置访问权限

Deploy an Nginx server

使用 kubectl get nodes 查看node列表

部署nginx需要 创建 kubernetes deployment

配置

# nginx_deploy.yaml
kind: Deployment
apiVersion: apps/v1
metadata:
  name: nginx
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx

  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.15
        ports:
        - containerPort: 80

$ kubectl create -f nginx_deploy.yaml
deployment.apps "nginx" created

$ kubectl get pod
NAME                   READY     STATUS    RESTARTS   AGE
nginx-c9bd9bc4-jqvb5   1/1       Running   0          1m

pod 运行起来了
使用 load balancer 访问

# nginx_svc.yaml
kind: Service
apiVersion: v1

metadata:
  name: nginx-elb
  namespace: default
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
    - name: http
      port: 80
      targetPort: 80

LoadBalancer 服务 转发流量到nginx

Deploy the Phoenix Chat

配置

kind: Deployment
apiVersion: apps/v1
metadata:
  name: chat
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: chat

  template:
    metadata:
      labels:
        app: chat
    spec:
      containers:
      - name: phoenix-chat
        image: alvises/phoenix-chat-example:1_kops_chat
        ports:
        - containerPort: 4000
        env:
        - name: PORT
          value: "4000"
        - name: PHOENIX_CHAT_HOST
          value: "chat.poeticoding.com"

docker 镜像 https://cloud.docker.com/repository/docker/alvises/phoenix-chat-example

LoadBalancer类似

kind: Service
apiVersion: v1

metadata:
  name: chat-elb
  namespace: default
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"

spec:
  type: LoadBalancer
  selector:
    app: chat
  ports:
    - name: http
      port: 80
      targetPort: 4000

Multiple Chat replicas

只运行在一个节点 replicas 为 1

多节点需要分布式消息处理

2019/2/4 posted in  阅读笔记

ActiveRecord’s queries tricks 小记

原文 https://medium.com/rubyinside/active-records-queries-tricks-2546181a98dd

关联表join时使用条件

# User model
scope :activated, ->{
  joins(:profile).where(profiles: { activated: true })
}

更好的做法

# Profile model
scope :activated, ->{ where(activated: true) }
# User model
scope :activated, ->{ joins(:profile).merge(Profile.activated) }

关于 merge
https://apidock.com/rails/ActiveRecord/SpawnMethods/merge
https://api.rubyonrails.org/classes/ActiveRecord/SpawnMethods.html

嵌套join的差异

  • User has_one Profile
  • Profile has_many Skills
User.joins(:profiles).merge(Profile.joins(:skills))
=> SELECT users.* FROM users 
   INNER JOIN profiles    ON profiles.user_id  = users.id
   LEFT OUTER JOIN skills ON skills.profile_id = profiles.id
# So you'd rather use:
User.joins(profiles: :skills)
=> SELECT users.* FROM users 
   INNER JOIN profiles ON profiles.user_id  = users.id
   INNER JOIN skills   ON skills.profile_id = profiles.id
   

内链接和外连接

Exist query

存在和不存在

# Post
scope :famous, ->{ where("view_count > ?", 1_000) }
# User
scope :without_famous_post, ->{
  where(_not_exists(Post.where("posts.user_id = users.id").famous))
}
def self._not_exists(scope)
  "NOT #{_exists(scope)}"
end
def self._exists(scope)
  "EXISTS(#{scope.to_sql})"
end

Subqueries 子查询

比如查询部分用户(user)的帖子(post)

不好的做法

Post.where(user_id: User.created_last_month.pluck(:id))

这里的缺陷是将运行两个SQL查询:一个用于获取用户的ID,另一个用于从这些user_id获取帖子

这样写一个查询就可以了

Post.where(user_id: User.created_last_month)

基础

.to_sql 生成 SQL 语句字符串
.explain 获取查询分析

Booleans

对于User.where.not(tall: true)在pg下会生成
SELECT users.* FROM users WHERE users.tall <> 't'
这返回 tall 是 false 的 记录,不包括是null 的

包括null应该这么写

User.where("users.tall IS NOT TRUE")

or

User.where(tall: [false, nil])
2018/12/22 posted in  阅读笔记