Coder Social home page Coder Social logo

note's People

Contributors

iamtomas avatar jwenjian avatar

Stargazers

 avatar

note's Issues

database.yml 的 collation 属性

为确保统一排序集,可提前配置 collaction 属性如 utf8mb4_general_ci

注意:记住是提前配置,如果已经存在的数据库,则需要删掉重新执行 rake db:create

浅析 Sidekiq

定义:用于处理后台任务且依赖于 Redis 的工具

快速使用:

# 1. Gemfile
gem 'redis-rails'

gem 'request_store' # 替换 Thread.current
gem 'request_store-sidekiq' # 确保在处理每个作业后清除存储,以确保与 Rack 中的安全性和一致性

gem 'redis-namespace' # 方便多个应用使用一个 redis
gem 'sidekiq', '< 6'
gem 'sidekiq-cron' # sidekiq 调度插件

# 2. config/redis.yml redis 配置,可以在 config/applicaiton.rb 设置 Rails.cache_store 为 redis

# 3. config/initializers/sidekiq.rb 绑定 redis 配置

# 4. config/sidekiq.yml 配置 sidekiq 信息

# 可选
# config/sidekiq_cron.yml 可通过 cron 方式调度作业
# config/initializers/sidekiq_cron.rb 加载到 Rails 应用

源码分析:

image

启动 Sidekiq 后首先解析配置(比如 queues、concurrency 等),然后是接连启动心跳检查、任务拉取器 、worker 管理器 及 worker(如下图)

image

  • 心跳检查(start_heartbeat):检查 sidekiq 健康状态
  • 任务拉取器:从 retry、schedule 队列中找到计划时间小于当前时间的任务,并通过 lpush 塞到 redis 任务队列
  • worker 管理器: 上图已经介绍的挺清晰了
  • worker:循环中检查 redis 队列,通过 brpop 阻塞式得消费任务

好文分享:

Map 笔记

定义

本质上是一个hmap类型的指针,键值对通过哈希表来储存

image

使用时需注意

  • 键必须是字符串、整型或指针等可比较的类型
  • 无序
  • 动态容量
  • 使用make初始化,否则panic
  • 非并发安全

扩容步骤

  1. 若元素长度大于负载因子*map容量,则触发扩容
  2. 接着创建新map并进行rehash,为了避免散列冲突,将容量扩大至原先2倍
  3. 若哈希值相同,会使用链式哈希表将它们放在同一个桶
  4. 最后释放原map

扩容过程可能存在大量的rehash操作,从而导致性能受到影响。
那么可以在创建Map时指定较大的容量或者手动调用runtime.GC()来触发垃圾回收操作,以释放未使用的内存。

触发refresh时机

  • 元素数量超过容量的 2/3 时,触发扩容操作
  • 元素数量过少而容量过大时,触发收缩操作
  • 探测序列过长时,触发重排操作
    整个过程就是创建新的map存储rehash的元素,由于比较耗费性能,采用增量式 rehash 策略,在扩容和收缩时只会处理一部分元素,避免一次性处理过多元素导致性能下降

增量式rehash具体策略

  1. 将新数组的容量设置为旧数组的两倍或一半,并且将哈希表的增量计数器加一
  2. 在对哈希表进行操作时,如果发现增量计数器的值达到了一个阈值,就会开始进行增量式 rehash 操作,将一部分元素从旧数组中复制到新数组中,并且重新计算这些元素的哈希值
  3. 在完成一次增量式 rehash 操作后,会将哈希表的增量计数器清零

异常捕获

map会检测并发写并调用runtime.throw() 函数抛出异常,可以在业务层加锁(sync.Mutex或sync.RWMutext)或sync.map

sync.Map

与加锁的区别在于它无需加解锁,而是使用了更高级的算法。当多个 goroutine 同时访问 sync.Map 时,它会自动分配不同的段来存储数据,并且每个段都有自己的读写锁,以避免竞争条件。另外还拥有动态分片(并发读写的压力增加会动态调整分片的数量)以及零值操作(读取或删除不存在的key非常高效)等提高性能的手段

sync.Map的Load()方法用于获取指定键对应的值,其过程首先获取内部的读锁,确保不被其他goroutine,如果有其他的goroutine正在写入,则Load方法会被堵塞,直到写入完成

sync.Map Store()方法用于向映射中存储一个键值对,为了保持缓存层和底层 map 数据的一致性,sync.Map 使用了一种特殊的机制,具体流程如下:

  1. 首先会将键和值打包成一个 interface{} 类型的元素,并将该元素存储在一个 map 类型的缓存层中
  2. 当缓存层中的元素数量达到一定阈值(默认为256)时,sync.Map 会将缓存层中的所有元素都复制到一个新的底层 map 中,并将原来的底层 map 替换为新的底层 map
  3. 在底层 map 替换过程中,sync.Map 会先获取一个写锁,以确保没有其他 goroutine 在此期间对底层 map 进行读写操作
  4. sync.Map 将缓存层中的所有元素复制到新的底层 map 中,并在底层 map 上调用 Store() 方法来存储这些元素。这样,所有新的元素都被添加到新的底层 map 中,而原来的底层 map 中的元素则被保留下来。
  5. 当新的底层 map 中的元素添加完成后,sync.Map 会释放写锁,并将缓存层中的元素清空。这样,缓存层和底层 map 的数据就保持了一致性

sync.Map 使用缓存层和底层 map 之间的转换机制来避免锁的使用,从而提高了并发性能。然而,由于缓存层和底层 map 之间存在一定的延迟和不一致性,因此在一些特殊的场景下,可能会出现一些数据同步的问题,需要特别注意

参考

https://juejin.cn/post/7226153290051141692

浅析 GraphQL

目的

了解 GraphQL 使用场景及基本原理

简述

“What's GraphQL?”

是一个用于 API 的查询语言,在一些特定场景下,提供了更高效、更强大、更灵活的 REST 替代方案

抱着疑问,让我们看看 GraphQL与 REST API 都有哪些区别?

image

REST API

  1. 简单易懂
  2. 数据返回结构固定
  3. 允许在 HTTP 层缓存数据

GraphQL

  1. 对外暴露统一的地址
  2. 提供 Query(查询)、Mutation(更改/新增) 两种操作
  3. 接口返回结构灵活
  4. 需要配置额外的模块实现缓存

“Why use GraphQL?”

团队维度

  • 减轻后端团队额外的工作量
  • 最大程度给到前端自由度
  • ..

技术维度

  • 减少网络请求数量
  • 按需获取数据
  • 强类型接口定义,静态的声明接口和请求的结构
  • ..

原理

“客户端将请求参数发送到服务端,这一整个过程都做了什么?”

image

简单来说,分以下几个步骤

  1. 请求经过 GraphQL Client 转换成客户端 Schema,它其实是一段 query 开头的字符串,描述了调用哪个方法、传递什么参数及返回哪些字段等
  2. GraphQL Server 拿到这段 Schema 后,通过事先定义好的服务端 Schema ,对其进行解析校验并执行对应的 resolve 函数提供数据服务

那么服务端如何识别并响应客户端 Schema 这段字符串,分为三个阶段:解析 -> 校验 -> 执行

解析阶段

GraphQL 定义了一系列的 特征标识符 及 AST语法树规范,以便逐字符扫描( charCodeAt )从而识别客户端 Schema,最终解析的产出物为 document,比如:

{
  "kind":"Document",
  "definitions":[
  {
    "kind":"OperationDefinition",
    "operation":"query",
    "name":{
      "kind":"Name",
      "value":"DisplayMember",
      "loc":{
        "start":13,
        "end":26
      }
    },
    "selectionSet":{
      "kind":"SelectionSet",
      "selections":[
        {
          "kind":"Field",
          "alias":null,
          "name":{
            "kind":"Name",
            "value":"fetchByGender",
            "loc":{
              "start":37,
              "end":50
            }
          },
          "arguments":[
            {
              "kind":"Argument",
              "name":{
                "kind":"Name",
                "value":"gender",
                "loc":{
                  "start":51,
                  "end":57
                }
              },
              "value":{
                "kind":"StringValue",
                "value":"M",
                "loc":{
                  "start":59,
                  "end":62
                }
              },
              "loc":{
                "start":51,
                "end":62
              }
            }
          ],
...

如果客户端 Schema 不符合定义的规范,就会直接抛出语法异常 Syntax Error
这种结构化的报错信息在定位问题上非常方便

校验阶段

判断客户端 Schema 是否按照服务端 Schema 定义的方式获取数据,比如方法名是否有误,必填项是否有值等

举个例子,调用一个不存在的接口:

{
    "errors":[
        {
            "message":"Cannot query field {方法名} on type "Query". Did you mean {方法名}?",
            "locations":[
                {
                    "line":3,
                    "column":9
                }
            ]
        }
    ]
}

执行阶段

根据解析阶段的产出物 document与服务端 Schema ,定位到服务端 Schema 下具体接口的 resolve 函数并执行返回结果

思考

GraphQL 虽然弥补了 REST API 部分不足,但是也存在几个值得思考的问题

  1. 任意嵌套层级的查询语句
query Authors {
  author(id: "abc") {
    posts {
      author {
        posts {
          # ..
        }
      }
    }
  }
}

官方提供了不少解决方案,比如:

  • 超时
  • 最大查询深度 (Maximum Query Depth)
# Schema-level:
class MySchema < GraphQL::Schema
  # ...
  max_depth 10
end

# Query-level, which overrides the schema-level setting:
MySchema.execute(query_string, max_depth: 10)
  • 查询复杂度
# Constant complexity:
field :top_score, Integer, null: false, complexity: 10

# Dynamic complexity:
field :top_scorers, [PlayerType], null: false do
  argument :limit, Integer, limit: false, default_value: 5
  complexity ->(ctx, args, child_complexity) {
    if ctx[:current_user].staff?
      # no limit for staff users
      0
    else
      # `child_complexity` is the value for selections
      # which were made on the items of this list.
      #
      # We don't know how many items will be fetched because
      # we haven't run the query yet, but we can estimate by
      # using the `limit` argument which we defined above.
      args[:limit] * child_complexity
    end
  }
end

query {
  author(id: "abc") {    # complexity: 1
    posts(first: 5) {    # complexity: 5
      title              # complexity: 5
    }
  }
}

前面几种方案都是服务端层级的限制,但过程浪费了不少资源,是否可以提前在客户端拦截,从而避免资源浪费呢?
GraphQL 提供了客户端层级的解决方案:节流

  • 基于服务器时间
  • 基于查询复杂度
    对查询时间的成本与复杂度需要有一个很好的估计,具体的话可参考此文档
  1. N+1 问题
    GraphQL 其中一个解决方案: DataLoader,它的灵感来自 shopify/graphql-batch ,本质都是通过延迟加载,等收集完再使用单个查询加载所有数据,那它们是如何控制加载延迟的呢?
  • graphql-batch
    使用Promise.rb这个 gem,引入后Promise#sync方法会被注册用于延迟解析,执行查询时会返回一个 Promise对象,这也是执行引擎将其视为惰性值的原因
  • Dataloader
    使用的 Ruby3的 Fiber ,而 Fiber 类似于线程(可以手动暂停和恢复)
    扩展:为什么不直接通过 Rails 的 includes 解决呢?
  1. Breaking Changes
    遇到破坏性修改,比如删除了表的其中一个字段,该如何解决?
    GraphQL 提供了解决方案: GraphQL::SchemaComparator 支持在 CI 的阶段检测新的提交,本质是监听 Schema 结构
    参照 @hooopo 大佬的实践:

image

实践

代码实现如下:

#1.定义数据
module Types
  class HttpLogType < Types::BaseObject
    field :id, ID, null: false
    field :url, String
    # ..

    field :request_log, RequestLogType, null: false
  end
end

# 2.接口支持
module Queries::HttpLog
  class HttpLog < Queries::BaseQuery
    description 'Find an HttpLog by ID'

    argument :id,         ID,      required: true
    argument :request_id, Integer, required: true

    type Types::HttpLogType, null: true

    def resolve(id:, request_id:)
      ::HttpLog.has_request_log(request_id).find_by(id: id)
    end
  end
end

module Mutations
  class ResendHttpLog < BaseMutation
    argument :id, Integer, required: true

    type GraphQL::Types::JSON

    def resolve(id:)
      # ..
    end
  end
end

# 3.结果返回
query {
  httpLog(id: 753, requestId: 101) {
    id
    url
  }
}
# => 
{
  "data": {
    "httpLog": {
      "id": "753",
      "url": "http://cmb-gitaly.dev.gitee.work/cmbchina/0624/gitee/0624/PRchushihua/hooks/new"
    }
  }
}

mutation {
  resendHttpLog(input: { id: 1 })
}
# => 
{
  "data": {
    "resendHttpLog": {
      "message": "重发请求成功",
      "client_mutation_id": null
    }
  }
}

总结

正如官方文档说的 :
GraphQL and REST can actually co-exist in your stack. For example, you can abstract REST APIs behind a GraphQL server. This can be done by masking your REST endpoint into a GraphQL endpoint using root resolvers.

GraphQL 和 REST API 是可以结合使用的,这也是官方所推荐的方案
任何技术都有利弊,关键是结合场景权衡收益,找到适合自己的技术选型

Q&A

参考

https://www.modb.pro/u/462310
https://juejin.cn/post/6844903679640731655#heading-6
http://www.myriptide.com/introduce-graphql/
https://yuanlin.dev/posts/how-i-use-graphql-in-go-with-gqlgen
https://www.howtographql.com/advanced/4-security/
https://ruby-china.org/topics/41141

Kafka 常用命令

cd opt/bitnami/kafka/bin // 进入bin目录

unset JMX_PORT; // 避免端口号冲突

kafka-topics.sh --create --bootstrap-server localhost:9092 --topic <topic_name> --partitions 1 // 创建topic

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group <group_name> --describe // 查看某个消费者组的消费情况

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group <group_name> --topic <topic_name>:<partition_num1>, <partition_num2> --reset-offsets --to-offset <offset_num> -execute // 设置topic的offsets,需要先停止消费端消费,否则会报错

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list // group列表


初识 Kafka

架构图:

image

  1. 生产者通过 push 模式将消息发布到 broker
  2. zookeeper 负责管理集群配置,选举 leader,以及在 Consumer Group 发生变化时进行 rebalance
  3. 消费者使用 pull 模式从 broker 订阅并消费消息

定义:基于 zookeeper 的分布式消息流平台,另外支持多分区、多副本,同时也是一款开源的基于发布订阅模式的消息引擎系统

术语:

  • 生产者
  • 消费者
  • 消费者群组
  • 主题(Topic)表示一类消息
  • 分区(Partition)单个主题下的分区可以分布在不同机器,以此来实现其伸缩性
  • 偏移量 (Consumer Offset)它是一个不断递增的整数值,用来记录消费者发生重平衡时的位置,以便用来恢复数据
  • broker 表示 Kafka 服务器,它负责接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存
  • 副本 表示消息的备份,Kafka 定义了两类副本:领导者副本(Leader Replica) 和 追随者副本(Follower Replica),前者对外提供服务,后者只是被动跟随
  • 重平衡(Rebalance)是消费者组内某个消费者实例挂掉后,其他消费者实例会自动重新分配订阅主题分区的过程,也是 Kafka 消费者端实现高可用的重要手段

PostgreSQL 查询原理

image

Postgresql 执行 insert、delete、update、select 都是通过 postgres.c 里面的 exec_simple_query 方法,其基本流程:

  1. 启动事务
start_xact_command();
  1. 进行语法分析生成语法树
parsetree_list = pg_parse_query(query_string);
  1. 语义分析与查询重写,就是将简单的一个 select 语句拆分成多个部分,将 parse tree 转换成 query tree
querytree_list = pg_analyze_and_rewrite(parsetree, query_string, NULL, 0, NULL);
  1. 生成执行计划,这部分核心代码在planner.c中,是PG的Query Optimizer,它会根据表和索引的统计信息去计算不同路径的可能代价值,最后选出最优者
plantree_list = pg_plan_queries(querytree_list,CURSOR_OPT_PARALLEL_OK, NULL);
  1. 执行计划
        /*
         * Run the portal to completion, and then drop it (and the receiver).
         */
        (void) PortalRun(portal,
                         FETCH_ALL,
                         true,  /* always top level */
                         true,
                         receiver,
                         receiver,
                         completionTag);

TODO 继续深入

参考

  1. PostgreSQL查询语句执行过程
  2. PostgreSQL数据库内核分析

如何实现后台任务调度系统

poll.go

package poller

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"
)

type Poller struct {
	runtineGroup *goruntineGroup // 并发控制
	workNum      int             // 记录同时在运行的最大goroutine数

	sync.Mutex
	ready  chan struct{} // 某个goroutine已经准备好了
	metric *metric       // 统计当前在运行中的goroutine数量
}

func NewPoller(workerNum int) *Poller {
	return &Poller{
		runtineGroup: newRoutineGroup(),
		workNum:      workerNum,
		ready:        make(chan struct{}, 1),
		metric:       newMetric(),
	}
}

// 调度器
func (p *Poller) schedule() {
	p.Lock()
	defer p.Unlock()
	if int(p.metric.BusyWorkers()) >= p.workNum {
		return
	}

	select {
	case p.ready <- struct{}{}: // 只要满足当前goroutine数量小于最大goroutine数量 那么就通知poll去调度goroutine执行任务
	default:
	}
}

func (p *Poller) Poll(ctx context.Context) error {
	for {
		fmt.Println("=========")
		p.schedule()

		select {
		case <-p.ready:
		case <-ctx.Done():
			return nil
		}

	LOOP:
		for {
			select {
			case <-ctx.Done():
				break LOOP
			default:
				task, err := p.fetch(ctx)
				if err != nil {
					log.Println("fetch task error:", err.Error())
					break
				}
				fmt.Println(task)
				p.metric.IncBusyWorker() // 当前正在运行的goroutine+1
				p.runtineGroup.Run(func() {
					if err := p.execute(ctx, task); err != nil {
						log.Println("execute task error:", err.Error())
					}
				})
				break LOOP
			}
		}
	}
}

func (p *Poller) fetch(ctx context.Context) (string, error) {
	time.Sleep(1000 * time.Millisecond)
	return "task", nil
}

func (p *Poller) execute(ctx context.Context, task string) error {
	defer func() {
		p.metric.DecBusyWorker() // 执行完成之后 goroutine数量-1
		p.schedule()             // 重新调度下一个goroutine去执行任务 这一步是必须的
	}()
	return nil
}

metric.go

package poller

import "sync/atomic"

type metric struct {
	busyWorkers uint64
}

func newMetric() *metric {
	return &metric{}
}

func (m *metric) IncBusyWorker() uint64 {
	return atomic.AddUint64(&m.busyWorkers, 1)
}

func (m *metric) DecBusyWorker() uint64 {
	return atomic.AddUint64(&m.busyWorkers, ^uint64(0))
}

func (m *metric) BusyWorkers() uint64 {
	return atomic.LoadUint64(&m.busyWorkers)
}

goroutine_group.go

package poller

import "sync"

type goruntineGroup struct {
	waitGroup sync.WaitGroup
}

func newRoutineGroup() *goruntineGroup {
	return new(goruntineGroup)
}

func (g *goruntineGroup) Run(fn func())  {
	g.waitGroup.Add(1)

	go func ()  {
		defer g.waitGroup.Done()
		fn()
	}()
}

func (g *goruntineGroup) Wait()  {
	g.waitGroup.Wait()
}

测试

package main

import (
	"context"
	"fmt"
	"ta/poller"
	"go.uber.org/goleak"
	"testing"
)

func TestMain(m *testing.M)  {
	fmt.Println("start")
	goleak.VerifyTestMain(m)
}

func TestPoller(t *testing.T) {
	producer := poller.NewPoller(5)
	producer.Poll(context.Background())
}

参考资料:Go实现后台任务调度系统

状态机(AASM)实现持久化

问题:状态发生变化时,状态列会更新但不会保存

举个例子:调用 #close 将状态设置为 closed ,但不会自动保存

class Book < ActiveRecord::Base
  # ... 

  aasm_event :close do
    transitions :to => :closed, :from => [:opened]
  end

  # ...
end

解决方案:调用 close! 将设置状态并自动保存 AR 对象

参考:http://www.cache.one/read/15868593

K8s是如何工作的?

目的:K8s如何对Pod进行编排

架构图:
image

编排流程:
image

1、运维人员向 kube-apiserver 发出指令(我想干什么,我期望事情是什么状态)
2、API 响应命令,通过一系列认证授权,把 Pod 数据存储到 etcd,创建 Deployment 资源并初始化。(期望状态)
3、Controller 通过 list-watch 机制,监测发现新的 Deployment,将该资源加入到内部工作队列,发现该资源没有关联的 Pod 和 Replicaset,启用 Deployment Controller 创建 Replicaset 资源,再启用 Replicaset Controller 创建 Pod。
4、所有 Controller 被创建完成后,将 Deployment,Replicaset,Pod 资源更新存储到 etcd。
5、Scheduler 通过 list-watch 机制,监测发现新的 Pod,经过主机过滤、主机打分规则,将 Pod 绑定(binding)到合适的主机。
6、将绑定结果存储到etcd。
7、Kubelet 每隔 20s(可以自定义)向 Api Server 通过 NodeName 获取自身 Node 上所要运行的 Pod 清单,通过与自己的内部缓存进行比较,新增加 pod。
8、Kubelet 创建 Pod。
9、Kube-proxy 为新创建的 Pod 注册动态 DNS 到 CoreOS,给 Pod 的 Service 添加 iptables/ipvs 规则,用于服务发现和负载均衡。
10、Controller 通过 Control loop(控制循环)将当前 Pod 状态与用户所期望的状态做对比,如果当前状态与用户期望状态不同,则 Controller会将 Pod 修改为用户期望状态,实在不行会将此 Pod 删掉,然后重新创建 Pod。

参考资料:K8S架构原理及其工作流程

Kafka 3 废弃 ZooKeeper ?

在没有 ZooKeeper 的情况下运行 Kafka 有多便利?

  • 安装 Kafka 不用依赖 Zookeeper、Java 这些环境了
  • 减少了大多数的运维操作,比如说扩容、分区迁移,不必需要掌握 ZooKeeper 运行原理
  • 一个最小的分布式 Kafka 集群从六个异构的节点(三个 ZooKeeper 节点,三个 Kafka 节点)变成只三个节点(三个 Kafka 节点)

是否有不需要停机的平滑升级方案?

2.8 版本之后的 3.0 版本会是一个特殊的搭桥版本(bridge release),在这个版本中,Quorum Controller 会和老版本的基于 ZooKeeper 的 Controller 共存,而在之后的版本才会去掉旧的 Controller 模块。这意味着如果想要从 2.8 版本以下升级到 3.0 以后的某一个版本,比如说 3.1,则需要借由 3.0 版本实现两次“跳跃”,建议持续采用灰度的方式来进行业务的升级验证

参考:

M1 开发环境

brew

从官网下载(如果因网络问题拉不下来,可以下载国内的脚本)后,把镜像修改成国内

asdf

负责多环境管理(包括 node 、ruby、go等)

brew install asdf

git

不要使用系统自带的 git

brew install git

另外 M1 下通过 brew install bash-completion 方式无法实现 git 补全,而是采用下边方式

curl https://raw.githubusercontent.com/git/git/master/contrib/completion/git-completion.bash -o ~/.git-completion.bash

mkdir ~/.zsh

cp git-completion.bash ~/.zsh/.git-completion.bash

# ~/.zshrc
zstyle ':completion:*:*:git:*' script ~/.zsh/.git-completion.bash
fpath=(~/.zsh $fpath)
autoload -Uz compinit && compinit

terminal 显示 git 分支则可以采用以下方式

# ~/.zshrc
function parse_git_branch() {
    git branch 2> /dev/null | sed -n -e 's/^\* \(.*\)/[\1]/p'
}

setopt PROMPT_SUBST
export PROMPT='%F{grey}%n%f %F{cyan}%~%f %F{green}$(parse_git_branch)%f %F{normal}$%f '

mysql

brew install mysql

navicat premium 16.0.9 无需注册码

docker

brew install docker

k8s

网上查询下,下载规定版本的 docker desktop,可以通过镜像启动 k8s

helm

brew install helm

redis

brew install redis

java

官网下载

kafka

在 m1 芯片下通过 brew 安装始终启动不了,也确保了 java 和 zookeeper 环境存在,最后通过 docker 将 kafka 和 zookeeper 容器跑起来

---
version: '3'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.0.1
    container_name: zookeeper
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000

  broker:
    image: confluentinc/cp-kafka:7.0.1
    container_name: broker
    ports:
    # To learn about configuring Kafka for access across networks see
    # https://www.confluent.io/blog/kafka-client-cannot-connect-to-broker-on-aws-on-docker-etc/
      - "9092:9092"
    depends_on:
      - zookeeper
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181'
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_INTERNAL:PLAINTEXT
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,PLAINTEXT_INTERNAL://broker:29092
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1

rails

执行 bundle exec db:create 报错 "grpc_c.bundle fails to load on Apple M1 (wrong architecture)"

bundle config set force_ruby_platform true.
bundle

执行bundle 报错 "ld: library not found for -lzstd while bundle install for mysql2 gem Ruby on macOS Big Sur"

gem install mysql2 -v '0.5.3' -- --with-opt-dir=$(brew --prefix openssl) --with-ldflags=-L/opt/homebrew/Cellar/zstd/xxx/lib

注意:xxx代表zstd版本

待补充

setAttribute 覆盖 css 属性

举个例子

<div class="list" v-show=false>
  ...
</div>

我们都知道 v-show 底层其实就是改变了 display 属性,如果执行 document.querySelector('.list').setAttribute() ,它会将 display:none 覆盖

初识 Rack

将了解到

  • 什么是 Rack
  • Rack 实现原理
  • 如何使用 Rack
  • Rack 的作用及场景

什么是 Rake :一般 Ruby Web Application 包含 Framework(如 Rails、Sinatra 等)、Rack 层及 WebServer 层(如 puma、thin 等)三个部分,而 Rack 在这另外两者之间充当桥梁提供接口

image

Rack 实现原理 :一个可以响应 call 方法的 Ruby 对象,它仅接受来自外界的一个参数,也就是环境,然后返回一个只包含三个值的数组,按照顺序分别是状态码、HTTP Headers 以及响应请求的正文

image

如何使用 Rack :可以先从 Rack 是如何启动 WebServer 这个角度展开

首先在任意目录下创建如下所示的 config.ru 文件

run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }

因为 Proc 对象也能够响应 #call 方法,所以上述的 Proc 对象也可以看做是一个 Rack 应用

接着可以使用 rackup 命令在命令行中启动一个 WebServer 进程

参考:

冒泡排序

基本**:两两比较相邻记录的关键字,如果是反序则交换,直到没有反序为止

特性:

image

代码实现:

  • Java Code
class Solution {
    public int[] sortArray(int[] nums) {
        int len = nums.length;
        //标志位
        boolean flag = true;
        //注意看 for 循环条件
        for (int i = 0; i < len && flag; ++i) {
            //如果没发生交换,则依旧为false,下次就会跳出循环
            flag = false;
            for (int j = 0; j < len - i - 1; ++j) {
                if (nums[j] > nums[j+1]) {
                    swap(nums,j,j+1);
                    //发生交换,则变为true,下次继续判断
                    flag = true;
                }
            }
        }
        return nums;

    }
    public void swap(int[] nums,int i,int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}
  • Ruby Code
def solution(nums)
  return [] unless nums.class == Array

  flag = true
  len = nums.size
  i = 0
  j = 0

  while (i < len) && flag
    flag = false
    while j < len - i - 1
      if nums[j] > nums[j+1]
        nums[j],nums[j+1] = nums[j+1],nums[j] 
        flag = true
      end
      j += 1
    end
    i += 1
    j = 0
  end
  nums
end

参考:https://github.com/chefyuan/algorithm-base/blob/main/animation-simulation/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F.md

搭建 Rspec 测试环境

为了方便 TDD 驱动而搭建的测试环境,它不仅可以执行完 rspec 后清理数据库,还能在执行 rspec 时自动加载 seed

# ...
require 'rake'
# ...
RSpec.configure do |config|
# ...
  config.before(:suite) do
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.clean_with(:truncation)
    Rails.application.load_tasks
    Rake::Task['db:seed'].invoke # loading seeds
  end
  
  config.around(:each) do |example|
    DatabaseCleaner.cleaning do
      example.run
    end
  end
end

参考:https://ruby-china.org/topics/30233

如何本地不安装 PostgreSQL 情况下执行 gem install pg

Docker 安装 PostgreSQL 更方便, 但 Gemfile 要求在本地安装,如何在本地不安装 PostgreSQL 下执行 gem install pg

libpq 是用 c 语言写的 postgreSQL 的程序接口 。在 Mac OS 环境,可以通过 brew install libpq 安装

然后指定 gem 的构建选项,命令如下:

gem install pg -- --with-opt-dir="/usr/local/opt/libpq"

如果使用bundle ,也需要指定一个配置目录,命令如下:

bundle config --local build.pg --with-opt-dir="/usr/local/opt/libpq"
bundle install

最后启动 Rails 程序就可以连接 Docker 里面的 PostgreSQL 了,减少本地安装依赖的开销

GitLab CI/CD 实践

在如今人人都卷的时代,当然是要花最少的时间干最多的事。检验学习成果当然是实践出真知,但在此之前先看看工作流与基本概念

image

传统的前端部署往往要经历这几个阶段:

  1. 本地代码更新
  2. 本地打包项目
  3. 清空服务器相应目录
  4. 上传项目包至相应目录

如何将这些机械重复的步骤实现自动化?

CI/CD(Continuous Intergration/Continuous Deploy )

简单来说就是持续集成/持续部署,它是GitLab内置的工具,能通过持续方法进行软件开发:

  1. Continuous Integration (CI) 持续集成
  2. Continuous Delivery (CD) 持续交付
  3. Continuous Deployment (CD) 持续部署

持续集成就是将小的代码块推送到Git仓库中托管的应用程序代码库中,并且每次推送时,都要运行一系列脚本来构建、测试和验证代码更改,然后再将其合并到主分支中,而持续交付和部署相当于更进一步的CI,可以在每次推送到仓库默认分支的同时将应用程序部署到生产环境,这些方法使得可以在开发周期的早期发现BUG和错误,从而确保部署到生产环境的所有代码都符合为应用程序建立的代码标准

持续部署的工具有挺多,比如Jenkins、Travis CI及GitLab CI/CD,这次先介绍GitLab CI/CD

GitLab CI

GitLab CI 是为 GitLab 提供持续集成服务的一整套系统,使用 GitLab CI 需要在仓库根目录创建一个gitlab-ci.yml的文件,它用来指定持续集成需要运行的环境,以及要执行的脚本。还需要设置一个 GitLab Runner ,当有代码变更的时候,它会自动开始 pipeline ,并在gitlab上显示持续集成的结果

在8.0以后默认集成且启用 GitLab CI

GitLab Runner

Runner 在我看来可以理解为一个个 worker、process ,它能配合CI使用,具体的流程:

  1. push 代码
  2. GitLab 通知 GitLab CI
  3. GitLab-CI 会找出与这个项目相关联的 Runner,并通知这些 Runner 把代码更新到本地并执行预定义好的执行脚本

image

简单介绍完概念后可以快速开始实践

快速开始

创建 .gitlab-ci.yml

配置 gitlab-ci.yml 告诉CI要对你的项目做什么,仓库一旦收到任何推送,GitLab将立即查找.gitlab-ci.yml文件,并根据文件的内容在Runner上启动作业

# 定义在每个job之前运行的命令
before_script:
  - echo '在每个job之前运行的命令'

# 定义在每个job之后运行的命令
after_script:
  - echo '在每个job之后运行的命令'

# 定义全局变量
variables:
  PROJECT: gitlab-rails-demo
  IMAGE_NAME: xxx/gitlab-rails-demo
  IMAGE_TAG: $CI_PIPELINE_ID

# Cache 在使用时制定一系列的文件或者文件目录,使得其在不同的 job 之间被缓存下来
# https://zhuanlan.zhihu.com/p/106971627
cache:
  untracked: true
  key: $PROJECTt-$CI_COMMIT_REF_NAME
  paths:
    - public/

stages:
  - build   # 构建
  - test   # 测试
  - deploy   # 部署

build_job:
  stage: build
  tags:
    - gitlab-rails-demo
  only:
    - dev
    - master
  except: # 排除某些分支
    - new-dev
  script:
    - echo '这里是构建docker镜像阶段'
    - docker login xxx --username=$DOCKER_REGISTRY_USERNAME --password=$DOCKER_REGISTRY_PASSWORD
    - docker build -t $IMAGE_NAME:$IMAGE_TAG --network webapp .  # 构建docker镜像
    - docker push $IMAGE_NAME:$IMAGE_TAG # 推送镜像

test_job:
  stage: test
  tags:
    - gitlab-rails-demo
  only:
    - dev
    - master
  script:
    - ls -a
    - echo '这里是测试阶段'
  allow_failure: true # 设置 allow_failure 的 job 失败后不会中断 CI 的执行

deploy_job:
  stage: deploy
  tags:
    - gitlab-rails-demo
  only:
    - dev
    - master
  script:
    - echo '这里是部署阶段'
    - docker-compose up -d # docker-compose
  after_script:
    - echo y | docker image prune --filter="dangling=true"  # 清理dangling镜像
    - docker images

注册 Runner

GitLab仓库下Settings-CI/CD按指导注册

拓展配置

DockerFile

# 使用安装了最少的软件的镜像
FROM ruby:2.7.4-alpine3.14

# 安装依赖
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk update && apk add --no-cache libpq vim imagemagick sqlite-libs tzdata
RUN apk add --no-cache --virtual .temp-build-libs \
    alpine-sdk gcc musl-dev build-base libffi-dev make sqlite-dev nodejs yarn

# 设置环境变量
# 来指代容器中的 rails 程序放的目录
ENV RAILS_ROOT /var/www/gitlab-rails-demo
ENV NODE_ENV production
ENV RAILS_ENV production
ENV HOST 0.0.0.0
ENV PORT 3000
ENV PIDFILE $RAILS_ROOT/tmp/pids/server.pid
ENV RAILS_MASTER_KEY 404aa1c143755a2541eba84121c52671

# 设置容器里的工作目录
WORKDIR $RAILS_ROOT

# 创建 rails 程序目录和程序运行所需要的 pids 的目录
RUN mkdir -p $RAILS_ROOT/tmp/pids

# 拷贝 docker-entrypoint.sh
COPY ./docker-entrypoint.sh .
RUN sed -i 's/\r$//g'  $RAILS_ROOT/docker-entrypoint.sh

# 拷贝 Gemfile 及 lock到容器的工作目录中
# 当Gemfile 没有改变时,省略下面的 bundle install
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock

# 安装 Rails 环境
RUN bundle install

# 拷贝js依赖
COPY package.json yarn.lock /app/

# 将 Dockerfile 目录下所有内容复制到容器工作目录
COPY . .

# 编译静态文件
RUN bundle exec rails webpacker:verify_install
RUN SECRET_KEY_BASE=nein bundle exec rails assets:precompile

# 删除依赖
RUN apk del .temp-build-libs

# 可执行权限
RUN chmod +x  $RAILS_ROOT/docker-entrypoint.sh

# 暴露服务端口
EXPOSE 3000

# 运行 docker-entrypoint.sh
ENTRYPOINT ["./docker-entrypoint.sh"]

docker-entrypoint.sh

#!/bin/sh

# 启动服务
bundle exec puma -C config/puma.rb

exec "$@"

docekr-compose

version: '3.9'
services:
  gitlab-rails-demo:
    container_name: gitlab-rails-demo
    image: xxx/gitlab-rails-demo:${IMAGE_TAG:-latest}
    networks:
      - webapp
    restart: always
    expose:
      - 3000
    volumes:
      - '/data/docker/gitlab-rails-demo/log:/var/www/gitlab-rails-demo/log'
networks:
  webapp:
    name: webapp
    driver: bridge

nginx

upstream gitlab-rails-demo-puma {
  server gitlab-rails-demo:3000;
}

## Redirects all HTTP traffic to the HTTPS host
server {
  listen 80;
  server_name rails.explorexd.com;
  access_log  /var/log/nginx/gitlab-rails-demo.log;
  error_log   /var/log/nginx/gitlab-rails-demo.log;

	location / {
	  client_max_body_size 256m;
	  gzip on;

	  proxy_read_timeout      300;
	  proxy_connect_timeout   300;
	  proxy_redirect          off;

	  proxy_http_version 1.1;

	  proxy_set_header    Host                $http_host;
	  proxy_set_header    X-Real-IP           $remote_addr;
	  proxy_set_header    X-Forwarded-Ssl     on;
	  proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
	  proxy_set_header    X-Forwarded-Proto   $scheme;
	  proxy_pass http://gitlab-rails-demo-puma;
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.