雪

查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

雪 提出了问题 · 8月24日

rabbitmq 消息发布与订阅

客户端(安卓跟IOS)用mqtt协议订阅消息,然后管理后台用amqp来发送消息,但是发现用ampq协议来发送西消息,mqtt端接收不到,如果用mqtt来发送是没有问题的,代码就是用官方的测试代码如下:


    // 发送端代码
    conn, err := amqp.Dial("amqp://guest:guest@:5672/")
    failOnError(err, "Failed to connect to RabbitMQ")
    defer conn.Close()
    ch, err := conn.Channel()
    failOnError(err, "Failed to open a channel")
    defer ch.Close()
    err = ch.ExchangeDeclare(
        "amq.topic", // name
        "topic",     // type
        true,        // durable
        false,       // auto-deleted
        false,       // internal
        false,       // no-wait
        nil,         // arguments
    )
    failOnError(err, "Failed to declare an exchange")
    body := bodyFrom(os.Args)
    routingKey := severityFrom(os.Args)
    fmt.Println(routingKey)
    err = ch.Publish(
        "amq.topic", // exchange
        routingKey,  // routing key
        false,       // mandatory
        false,       // immediate
        amqp.Publishing{
            ContentType: "text/plain",
            Body:        []byte(body),
        })
    failOnError(err, "Failed to publish a message")
    log.Printf(" [x] Sent %s", body)
}
    // 客户端订阅接收代码
    client := connect("sub", host)
    client.Subscribe("/bin/#", 0, func(client mqtt.Client, msg mqtt.Message) {
        fmt.Printf("* [%s] >> %s\n", msg.Topic(), string(msg.Payload()))
    })

关注 1 回答 0

雪 提出了问题 · 1月13日

关于视频带宽问题?

大佬们好,请教个问题,加入我要推一个直播流,分辨率是1920*1080,码率是5000kb/s, 帧率是60fps,那么需要多大的带宽才能不卡或者掉帧呢?具体怎么计算的?

关注 1 回答 1

雪 提出了问题 · 2019-01-02

关于K8S 1.13.1版本storageclass自动创建PV的问题

原先是在本地的测试机,环境是1.9.2的k8s单机版测试的,配置文件主要是如下三个:

provisioner.yml

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: nfs-client-deploy
spec:
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: nfs-client-deploy
    spec:
      serviceAccount: nfs-client-provisioner
      containers:
        - name: nfs-client-provisioner
          image: registry.cn-hangzhou.aliyuncs.com/open-ali/nfs-client-provisioner
          volumeMounts:
            - name: nfs-client
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: my-provisioner
            - name: NFS_SERVER
              value: 192.168.163.184
            - name: NFS_PATH
              value: /root/docker
      volumes:
        - name: nfs-client
          nfs:
            server: 192.168.163.184
            path: /root/docker

storageclass.yml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
provisioner: -provisioner

test-claim.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: nfs-storage
  resources:
    requests:
      storage: 1Mi

上面两个文件主要是创建storageclss的,最后一个主要是一个测试;
现在的问题 在我的测试环境的集群中 pvc一直是pending状态,如下图:图片描述

然后相应的pod那里自然也是一直在pending中,通过describe查看提示:
pod has unbound immediate PersistentVolumeClaims (repeated 4 times)
,不知道什么问题导致,之前在1.9.2的版本中OK,请大神指导。

关注 3 回答 2

雪 收藏了文章 · 2018-12-14

Golang - 调度剖析【第三部分】

本篇是调度剖析的第三部分,将重点关注并发特性。
回顾:
第一部分
第二部分

简介

首先,在我平时遇到问题的时候,特别是如果它是一个新问题,我一开始并不会考虑使用并发的设计去解决它。我会先实现顺序执行的逻辑,并确保它能正常工作。然后在可读性和技术关键点都 Review 之后,我才会开始思考并发执行的实用性和可行性。有的时候,并发执行是一个很好的选择,有时则不一定。

在本系列的第一部分中,我解释了系统调度的机制和语义,如果你打算编写多线程代码,我认为这些机制和语义对于实现正确的逻辑是很重要的。在第二部分中,我解释了Go 调度的语义,我认为它能帮助你理解如何在 Go 中编写高质量的并发程序。在这篇文章中,我会把系统调度Go 调度的机制和语义结合在一起,以便更深入地理解什么才是并发以及它的本质。

什么是并发

并发意味着乱序执行。拿一组原来是顺序执行的指令,而后找到一种方法,使这些指令乱序执行,但仍然产生相同的结果。那么,顺序执行还是乱序执行?根本在于,针对我们目前考虑的问题,使用并发必须是有收益的!确切来说,是并发带来的性能提升要大于它带来的复杂性成本。当然有些场景,代码逻辑就已经约束了我们不能执行乱序,这样使用并发也就没有了意义。

并发与并行

理解并发并行的不同也非常重要。并行意味着同时执行两个或更多指令,简单来说,只有多个CPU核心之间才叫并行。在 Go 中,至少要有两个操作系统硬件线程并至少有两个 Goroutine 时才能实现并行,每个 Goroutine 在一个单独的系统线程上执行指令。

如图:

图片描述
我们看到有两个逻辑处理器P,每个逻辑处理器都挂载在一个系统线程M上,而每个M适配到计算机上的一个CPU处理器Core
其中,有两个 Goroutine G1G2并行执行,因为它们同时在各自的系统硬件线程上执行指令。
再看,在每一个逻辑处理器中,都有三个 Goroutine G2 G3 G5G1 G4 G6 轮流共享各自的系统线程。看起来就像这三个 Goroutine 在同时运行着,没有特定顺序地执行它们的指令,并在系统线程上共享时间。
那么这就会发生竞争,有时候如果只在一个物理核心上实现并发则实际上会降低吞吐量。还有有意思的是,有时候即便利用上了并行的并发,也不会给你带来想象中更大的性能提升。

工作负载

我们怎么判断在什么时候并发会更有意义呢?我们就从了解当前执行逻辑的工作负载类型开始。在考虑并发时,有两种类型的工作负载是很重要的。

两种类型

CPU-Bound:这是一种不会导致 Goroutine 主动切换上下文到等待状态的类型。它会一直不停地进行计算。比如说,计算 π 到第 N 位的 Goroutine 就是 CPU-Bound 的。

IO-Bound:与上面相反,这种类型会导致 Goroutine 自然地进入到等待状态。它包括请求通过网络访问资源,或使用系统调用进入操作系统,或等待事件的发生。比如说,需要读取文件的 Goroutine 就是 IO-Bound。我把同步事件(互斥,原子),会导致 Goroutine 等待的情况也包含在此类。

CPU-Bound 中,我们需要利用并行。因为单个系统线程处理多个 Goroutine 的效率不高。而使用比系统线程更多的 Goroutine 也会拖慢执行速度,因为在系统线程上切换 Goroutine 是有时间成本的。上下文切换会导致发生STW(Stop The World),意思是在切换期间当前工作指令都不会被执行。

IO-Bound 中,并行则不是必须的了。单个系统线程可以高效地处理多个 Goroutine,是因为Goroutine 在执行这类指令时会自然地进入和退出等待状态。使用比系统线程更多的 Goroutine 可以加快执行速度,因为此时在系统线程上切换 Goroutine 的延迟成本并不会产生STW事件。进入到IO阻塞时,CPU就闲下来了,那么我们可以使不同的 Goroutine 有效地复用相同的线程,不让系统线程闲置。

我们如何评估一个系统线程匹配多少 Gorountine 是最合适的呢?如果 Goroutine 少了,则会无法充分利用硬件;如果 Goroutine 多了,则会导致上下文切换延迟。这是一个值得考虑的问题,但此时暂不深究。

现在,更重要的是要通过仔细推敲代码来帮助我们准确识别什么情况需要并发,什么情况不能用并发,以及是否需要并行。

加法

我们不需要复杂的代码来展示和理解这些语义。先来看看下面这个名为add的函数:

1 func add(numbers []int) int {
2      var v int
3     for _, n := range numbers {
4         v += n
5     }
6     return v
7 }

在第 1 行,声明了一个名为add的函数,它接收一个整型切片并返回切片中所有元素的和。它从第 2 行开始,声明了一个v变量来保存总和。然后第 3 行,线性地遍历切片,并且每个数字被加到v中。最后在第 6 行,函数将最终的总和返回给调用者。

问题:add函数是否适合并发执行?从大体上来说答案是适合的。可以将输入切片分解,然后同时处理它们。最后将每个小切片的执行结果相加,就可以得到和顺序执行相同的最终结果。

与此同时,引申出另外一个问题:应该分成多少个小切片来处理是性能最佳的呢?要回答此问题,我们必须知道它的工作负载类型。
add函数正在执行 CPU-Bound 工作负载,因为实现算法正在执行纯数学运算,并且它不会导致 Goroutine 进入等待状态。这意味着每个系统线程使用一个 Goroutine 就可以获得不错的吞吐量。

并发版本

下面来看一下并发版本如何实现,声明一个 addConcurrent 函数。代码量相比顺序版本增加了很多。

1 func addConcurrent(goroutines int, numbers []int) int {
2     var v int64
3     totalNumbers := len(numbers)
4     lastGoroutine := goroutines - 1
5     stride := totalNumbers / goroutines
6
7     var wg sync.WaitGroup
8     wg.Add(goroutines)
9
10     for g := 0; g < goroutines; g++ {
11         go func(g int) {
12             start := g * stride
13             end := start + stride
14             if g == lastGoroutine {
15                 end = totalNumbers
16             }
17
18             var lv int
19             for _, n := range numbers[start:end] {
20                 lv += n
21             }
22
23             atomic.AddInt64(&v, int64(lv))
24             wg.Done()
25         }(g)
26     }
27
28     wg.Wait()
29
30     return int(v)
31 }

第 5 行:计算每个 Goroutine 的子切片大小。使用输入切片总数除以 Goroutine 的数量得到。
第 10 行:创建一定数量的 Goroutine 执行子任务
第 14-16 行:子切片剩下的所有元素都放到最后一个 Goroutine 执行,可能比前几个 Goroutine 处理的数据要多。
第 23 行:将子结果追加到最终结果中。

然而,并发版本肯定比顺序版本更复杂,但和增加的复杂性相比,性能有提升吗?值得这么做吗?让我们用事实来说话,下面运行基准测试。

基准测试

下面的基准测试,我使用了1000万个数字的切片,并关闭了GC。分别有顺序版本add函数和并发版本addConcurrent函数。

func BenchmarkSequential(b *testing.B) {
    for i := 0; i < b.N; i++ {
        add(numbers)
    }
}

func BenchmarkConcurrent(b *testing.B) {
    for i := 0; i < b.N; i++ {
        addConcurrent(runtime.NumCPU(), numbers)
    }
}

无并行

以下是所有 Goroutine 只有一个硬件线程可用的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU8 Goroutines。在这种情况下,并发版本实际正跑在没有并行的机制上。

10 Million Numbers using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential              1000       5720764 ns/op : ~10% Faster
BenchmarkConcurrent              1000       6387344 ns/op
BenchmarkSequentialAgain         1000       5614666 ns/op : ~13% Faster
BenchmarkConcurrentAgain         1000       6482612 ns/op

结果表明:当只有一个系统线程可用于所有 Goroutine 时,顺序版本比并发快约10%到13%。这和我们之前的理论预期相符,主要就是因为并发版本在单核上的上下文切换和 Goroutine 管理调度的开销。

有并行

以下是每个 Goroutine 都有单独可用的系统线程的结果。顺序版本使用 1 Goroutine,并发版本在我的机器上使用runtime.NumCPU8 Goroutines。在这种情况下,并发版本利用上了并行机制。

10 Million Numbers using 8 goroutines with 8 cores
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 8 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/cpu-bound
BenchmarkSequential-8                1000       5910799 ns/op
BenchmarkConcurrent-8                2000       3362643 ns/op : ~43% Faster
BenchmarkSequentialAgain-8           1000       5933444 ns/op
BenchmarkConcurrentAgain-8           2000       3477253 ns/op : ~41% Faster

结果表明:当为每个 Goroutine 提供单独的系统线程时,并发版本比顺序版本快大约41%到43%。这才也和预期一致,所有 Goroutine 现都在并行运行着,意味着他们真的在同时执行。

排序

另外,我们也要知道并非所有的 CPU-Bound 都适合并发。当切分输入或合并结果的代价非常高时,就不太合适。下面展示一个冒泡排序算法来说明此场景。

顺序版本

01 package main
02
03 import "fmt"
04
05 func bubbleSort(numbers []int) {
06     n := len(numbers)
07     for i := 0; i < n; i++ {
08         if !sweep(numbers, i) {
09             return
10         }
11     }
12 }
13
14 func sweep(numbers []int, currentPass int) bool {
15     var idx int
16     idxNext := idx + 1
17     n := len(numbers)
18     var swap bool
19
20     for idxNext < (n - currentPass) {
21         a := numbers[idx]
22         b := numbers[idxNext]
23         if a > b {
24             numbers[idx] = b
25             numbers[idxNext] = a
26             swap = true
27         }
28         idx++
29         idxNext = idx + 1
30     }
31     return swap
32 }
33
34 func main() {
35     org := []int{1, 3, 2, 4, 8, 6, 7, 2, 3, 0}
36     fmt.Println(org)
37
38     bubbleSort(org)
39     fmt.Println(org)
40 }

这种排序算法会扫描每次在交换值时传递的切片。在对所有内容进行排序之前,可能需要多次遍历切片。

那么问题:bubbleSort函数是否适用并发?我相信答案是否定的。原始切片可以分解为较小的,并且可以同时对它们排序。但是!在并发执行完之后,没有一个有效的手段将子结果的切片排序合并。下面我们来看并发版本是如何实现的。

并发版本

01 func bubbleSortConcurrent(goroutines int, numbers []int) {
02     totalNumbers := len(numbers)
03     lastGoroutine := goroutines - 1
04     stride := totalNumbers / goroutines
05
06     var wg sync.WaitGroup
07     wg.Add(goroutines)
08
09     for g := 0; g < goroutines; g++ {
10         go func(g int) {
11             start := g * stride
12             end := start + stride
13             if g == lastGoroutine {
14                 end = totalNumbers
15             }
16
17             bubbleSort(numbers[start:end])
18             wg.Done()
19         }(g)
20     }
21
22     wg.Wait()
23
24     // Ugh, we have to sort the entire list again.
25     bubbleSort(numbers)
26 }

bubbleSortConcurrent它使用多个 Goroutine 同时对输入的一部分进行排序。我们直接来看结果:

Before:
  25 51 15 57 87 10 10 85 90 32 98 53
  91 82 84 97 67 37 71 94 26  2 81 79
  66 70 93 86 19 81 52 75 85 10 87 49

After:
  10 10 15 25 32 51 53 57 85 87 90 98
   2 26 37 67 71 79 81 82 84 91 94 97
  10 19 49 52 66 70 75 81 85 86 87 93

由于冒泡排序的本质是依次扫描,第 25 行对 bubbleSort 的调用将掩盖使用并发解决问题带来的潜在收益。结论是:在冒泡排序中,使用并发不会带来性能提升。

读取文件

前面已经举了两个 CPU-Bound 的例子,下面我们来看 IO-Bound

顺序版本

01 func find(topic string, docs []string) int {
02     var found int
03     for _, doc := range docs {
04         items, err := read(doc)
05         if err != nil {
06             continue
07         }
08         for _, item := range items {
09             if strings.Contains(item.Description, topic) {
10                 found++
11             }
12         }
13     }
14     return found
15 }

第 2 行:声明了一个名为 found 的变量,用于保存在给定文档中找到指定主题的次数。
第 3-4 行:迭代文档,并使用read函数读取每个文档。
第 8-11 行:使用 strings.Contains 函数检查文档中是否包含指定主题。如果包含,则found加1。

然后来看一下read是如何实现的。

01 func read(doc string) ([]item, error) {
02     time.Sleep(time.Millisecond) // 模拟阻塞的读
03     var d document
04     if err := xml.Unmarshal([]byte(file), &d); err != nil {
05         return nil, err
06     }
07     return d.Channel.Items, nil
08 }

此功能以 time.Sleep 开始,持续1毫秒。此调用用于模拟在我们执行实际系统调用以从磁盘读取文档时可能产生的延迟。这种延迟的一致性对于准确测量find顺序版本和并发版本的性能差距非常重要。
然后在第 03-07 行,将存储在全局变量文件中的模拟 xml 文档反序列化为struct值。最后,将Items返回给调用者。

并发版本

01 func findConcurrent(goroutines int, topic string, docs []string) int {
02     var found int64
03
04     ch := make(chan string, len(docs))
05     for _, doc := range docs {
06         ch <- doc
07     }
08     close(ch)
09
10     var wg sync.WaitGroup
11     wg.Add(goroutines)
12
13     for g := 0; g < goroutines; g++ {
14         go func() {
15             var lFound int64
16             for doc := range ch {
17                 items, err := read(doc)
18                 if err != nil {
19                     continue
20                 }
21                 for _, item := range items {
22                     if strings.Contains(item.Description, topic) {
23                         lFound++
24                     }
25                 }
26             }
27             atomic.AddInt64(&found, lFound)
28             wg.Done()
29         }()
30     }
31
32     wg.Wait()
33
34     return int(found)
35 }

第 4-7 行:创建一个channel并写入所有要处理的文档。
第 8 行:关闭这个channel,这样当读取完所有文档后就会直接退出循环。
第 16-26 行:每个 Goroutine 都从同一个channel接收文档,readstrings.Contains 逻辑和顺序的版本一致。
第 27 行:将各个 Goroutine 计数加在一起作为最终计数。

基准测试

同样的,我们再次运行基准测试来验证我们的结论。

func BenchmarkSequential(b *testing.B) {
    for i := 0; i < b.N; i++ {
        find("test", docs)
    }
}

func BenchmarkConcurrent(b *testing.B) {
    for i := 0; i < b.N; i++ {
        findConcurrent(runtime.NumCPU(), "test", docs)
    }
}

无并行

10 Thousand Documents using 8 goroutines with 1 core
2.9 GHz Intel 4 Core i7
Concurrency WITHOUT Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -cpu 1 -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential                 3    1483458120 ns/op
BenchmarkConcurrent                20     188941855 ns/op : ~87% Faster
BenchmarkSequentialAgain            2    1502682536 ns/op
BenchmarkConcurrentAgain           20     184037843 ns/op : ~88% Faster

当只有一个系统线程时,并发版本比顺序版本快大约87%到88%。与预期一致,因为所有 Goroutine 都有效地共享单个系统线程。

有并行

10 Thousand Documents using 8 goroutines with 8 core
2.9 GHz Intel 4 Core i7
Concurrency WITH Parallelism
-----------------------------------------------------------------------------
$ GOGC=off go test -run none -bench . -benchtime 3s
goos: darwin
goarch: amd64
pkg: github.com/ardanlabs/gotraining/topics/go/testing/benchmarks/io-bound
BenchmarkSequential-8                   3    1490947198 ns/op
BenchmarkConcurrent-8                  20     187382200 ns/op : ~88% Faster
BenchmarkSequentialAgain-8              3    1416126029 ns/op
BenchmarkConcurrentAgain-8             20     185965460 ns/op : ~87% Faster

有意思的来了,使用额外的系统线程提供并行能力,实际代码性能却没有提升。也印证了开头的说法。

结语

我们可以清楚地看到,使用 IO-Bound 并不需要并行来获得性能上的巨大提升。这与我们在 CPU-Bound 中看到的结果相反。当涉及像冒泡排序这样的算法时,并发的使用会增加复杂性而没有任何实际的性能优势。
所以,我们在考虑解决方案时,首先要确定它是否适合并发,而不是盲目认为使用更多的 Goroutine 就一定会提升性能。

查看原文

雪 提出了问题 · 2018-11-26

PHP慢日志

最近我们线上在晚上高峰期的时候经常会出现502比较多的情况,经查询,在报警的时刻确实有很多满日志,但是看到了慢日志之后有点懵了,如图:图片描述

,慢日志都是在composer的classloader.php这个文件的322行代码,而这行代码只是调用了一个函数:

/**
 * Scope isolated include.
 *
 * Prevents access to $this/self from included files.
 */
function includeFile($file)
{
    include $file;
}

所以现在就有点懵了,而且我们线上是平行部署了10几台,只有其中的3台有这种情况,服务器负载啊之类的都正常,没有特殊飙高之类的情况。不知道大家有没有遇见过,求大神支招,有啥排查思路。。。

关注 1 回答 1

雪 提出了问题 · 2018-11-21

K8S新扩容POD没有流量

部署大概是这样子:一个svc部署的是NGINX,然后一个svc部署的fpm,然后NGINX通过svcname 代理到fpm,从而完成用nginx访问PHP项目, 现在的问题是:我把fpm的pod扩容了一半,但是发现请求并没有到这新的pod上,这是为什么呢?我把nginx的svc干掉了再创建就好了,这什么情况呢?

关注 1 回答 0

雪 提出了问题 · 2018-11-15

关于列表内容的缓存问题

举个栗子,像今日头像这种新闻类的APP,他的页面都是一条条新闻组成的,每条新闻还可以互动,如点赞,评论之类的,那么这种页面的数据肯定是经过CDN缓存过的,那么我的问题是:比如我对某个新闻点赞了,或者评论了,那么虽然实际数据已经执行点赞跟评论了,那么如果保证我页面刷新的时候是一个已经点赞和已经评论的状态呢?如果中间没有经过CDN的,那么我们可能就会触发式的去更新相关的内容数据了,不知道大家在处理这方面的事情都是怎么处理的?

关注 3 回答 2

雪 评论了文章 · 2018-11-13

在K8S集群中一步步构建一个复杂的MySQL数据库

文档说明

​ 本文面向容器初学者,作者先简单的用MySQL官方镜像搭建一个可运行的单实例数据库,而后考虑生产或现实需求,一步一步完善并揉合K8S多个技术,从而构建一个复杂且可供生产用的MySQL单实例库。

简单部署

​ 如下所示,我们仅需设置root用户密码(环境变量MYSQL_ROOT_PASSWORD), 便可轻松的使用MySQL官方镜像构建一个MySQL数据库。

# kubectl create -f - <<EOF
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: Changeme
EOF

注意:若你的K8S集群是minishiftopenshiftorigin,因其为安全考虑,不允许容器以root用户运行,而官方MySQL镜像却需root权限,故为使其能顺利运行,我们需将anyuid scc赋予default serviceaccount

# oc adm policy add-scc-to-user anyuid -z default

​ 创建一Service以便集群内外均可访问数据库,其中集群外需通过nodePort设置的30006端口访问。

# kubectl create -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  type: NodePort
  ports:
  - port: 3306
    nodePort: 30006
    protocol: TCP
    targetPort: 3306
  selector:
    app: mysql
EOF

​ 接着,访问数据库并验证其运行正常:

# kubectl get pod                                   # 当前Pod名称
NAME                     READY     STATUS    RESTARTS   AGE
mysql-5b5668c448-t44ml   1/1       Running   0          3h

# 通过本机访问
# kubectl exec -it mysql-5b5668c448-t44ml -- mysql -uroot -pChangeme

mysql> select 1;
+---+
| 1 |
+---+
| 1 |
+---+

# 集群内部通过mysql service访问:
# kubectl exec -it mysql-5b5668c448-t44ml -- mysql -uroot -pChangeme -hmysql

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2018-05-21 07:19:14 |
+---------------------+

# 集群外部,可通过任何一个K8S节点访问数据库:
# mysql -uroot -pChangeme -horigin-lb-01 -P30006

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

扩展部署

持久化存储

​ 若要确保MySQL重启后数据仍然存在,我们需为其配置可持久化存储,作者的实验环境配置了GlusterFS分布式存储,其支持K8S动态提供特性,故可执行如下命令创建PVC

# kubectl create -f - <<EOF
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mysql
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: glusterfs-raid0
EOF

​ 而后,调整Deploy并挂载卷:

    spec:
      containers:
      - image: mysql
...
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql

自定义配置文件

​ 通过创建cm并挂载到容器中,我们可自定义MySQL配置文件。如下所示,名为mysql-configcm包含一个custom.cnf文件:

apiVersion: v1
metadata:
  name: mysql-config
data:
  custom.cnf: |
        [mysqld]
        default_storage_engine=innodb
        skip_external_locking
        lower_case_table_names=1
        skip_host_cache
        skip_name_resolve
kind: ConfigMap

​ 将cm挂载到容器内:

    spec:
...
      containers:
      - image: mysql
...
        volumeMounts:
        - name: mysql-config
          mountPath: /etc/mysql/conf.d/
...
      volumes:
      - name: mysql-config
        configMap:
          name: mysql-config
...

加密铭感数据

​ 用户密码等铭感数据以Secret加密保存,而后被Deployment通过volume挂载或环境变量引用。如本例,我们创建rootapptest用户,将3个用户的密码加密保存:

# echo -n Changeme | base64
Q2hhbmdlbWU=
# kubectl create -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: mysql-user-pwd
data:
  mysql-root-pwd: Q2hhbmdlbWU=
  mysql-app-user-pwd: Q2hhbmdlbWU=
  mysql-test-user-pwd: Q2hhbmdlbWU=
EOF

Secret创建完成后,我们将用户明文密码从Deployment去除,采用环境变量方式引用Secret数据,参见如下Yaml修改,做了3个调整:

  • 镜像初始化时自动创建MYSQL_DATABASE环境变量1设置的数据库;
  • 镜像初始化时将MYSQL_DATABASE数据库赋予MYSQL_USER用户;
  • root用户及MYSQL_USER用户,其密码均通过secretKeyRefsecret获取;
    spec:
...
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-root-pwd
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-app-user-pwd
        - name: MYSQL_USER
          value: app
        - name: MYSQL_DATABASE
          value: appdb

容器健康检查

K8S镜像控制器可通过livenessProbe判断容器是否异常,进而决定是否重建容器;而Service服务可通过readinessProbe判断容器服务是否正常,从而确保服务可用性。

​ 本例,作者配置的livenessProbereadinessProbe是一样的,即连续3次查询数据库失败,则定义为异常。对livenessProbereadinessProbe详细用法,不在本文的讨论范围内,可参考K8S官方文档:

    spec:
      containers:
      - image: mysql
...
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 30
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
...

容器初始化

假设,我们有这样的需求:“初始部署MySQL时,其已包应用所需的数据库、用户、权限、表结构与数据”。研究MySQL官方镜像的Dockerfile可知,数据库初始化时将自动执行目录/docker-entrypoint-initdb.d内的.sh.sql.sql.gz文件,鉴于此,我们可有如下两种方法:

  1. 基于官方镜像重新编写Dockerfile,将脚本copy到新镜像/docker-entrypoint-initdb.d目录,因需编译新镜像,故此方法不灵活;
  2. 初始化容器(initContainers)在常规容器(containers)前运行,故在初始化容器中可将脚本拷贝到共享目录,而后MySQL镜像挂载此目录到/docker-entrypoint-initdb.d,此方法灵活。

本例,作者采用初始化容器方案,功能如下:

  • 初始化与常规容器共享名为mysql-initdbemptyDir,均被挂载到/docker-entrypoint-initdb.d目录;
  • 初始化容器将.sql文件置于共享的/docker-entrypoint-initdb.d目录,其含初始化testdbappdb数据库;
  • 为了避免MySQL数据库目录内的lost+found目录被误认为是数据库,初始化容器中将其删除;
    spec:
      initContainers:
      - name: mysql-init
        image: busybox
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_TEST_USER_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-test-user-pwd
        command:  
          - sh
          - "-c"
          - |
            set -ex
            rm -fr /var/lib/mysql/lost+found
            cat > /docker-entrypoint-initdb.d/mysql-testdb-init.sql <<EOF
            create database testdb default character set utf8;
            grant all on testdb.* to 'test'@'%' identified by '$MYSQL_TEST_USER_PASSWORD';
            flush privileges;
            EOF
            cat > /docker-entrypoint-initdb.d/mysql-appdb-init.sql <<EOF
            create table app(id int);
            insert into app values(1);
            commit;
            EOF
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
      containers:
      - image: mysql
        volumeMounts:
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
...
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql
      - name: mysql-initdb
        emptyDir: {}
...

完整Deployment

​ 通过如上多步调整,MySQL数据库的Deplyment如下所示:


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: mysql-init
        image: busybox
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_TEST_USER_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-test-user-pwd
        command:  
          - sh
          - "-c"
          - |
            set -ex
            rm -fr /var/lib/mysql/lost+found
            cat > /docker-entrypoint-initdb.d/mysql-testdb-init.sql <<EOF
            create database testdb default character set utf8;
            grant all on testdb.* to 'test'@'%' identified by '$MYSQL_TEST_USER_PASSWORD';
            flush privileges;
            EOF
            cat > /docker-entrypoint-initdb.d/mysql-appdb-init.sql <<EOF
            create table app(id int);
            insert into app values(1);
            commit;
            EOF
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-root-pwd
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-app-user-pwd
        - name: MYSQL_USER
          value: app
        - name: MYSQL_DATABASE
          value: appdb
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
        - name: mysql-config
          mountPath: /etc/mysql/conf.d/
        ports:
        - name: mysql
          containerPort: 3306
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 30
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql
      - name: mysql-initdb
        emptyDir: {}
      - name: mysql-config
        configMap:
          name: mysql-config

创建此Deployment后,我们有如下组件:

# kubectl get all,pvc,cm,secret
# MySQL Deployment:
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/mysql   1         1         1            1           1m

# RS被Deployment调用,其是自动生成的
NAME                 DESIRED   CURRENT   READY     AGE
rs/mysql-998977cdd   1         1         1         1m

# Pod:
NAME                       READY     STATUS    RESTARTS   AGE
po/mysql-998977cdd-v2ks2   1/1       Running   1          1m

# Service:
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
svc/mysql         NodePort    172.30.3.200    <none>        3306:30006/TCP   8h

# Pvc:
NAME            STATUS    VOLUME     CAPACITY   ACCESS MODES STORAGECLASS      AGE
pvc/mysql       Bound     pvc-fe..   1Gi        ROX          glusterfs-raid0   2m

# Configmap:
NAME              DATA      AGE
cm/mysql-config   1         6h

# Secret:
NAME                        TYPE                                  DATA      AGE
secrets/mysql-user-pwd      Opaque                                3         1h

定期自动备份

​ 考虑到数据安全性,我们定期备份数据库,在K8S集群中,我们可配置CronJob实现自动备份作业。首先,创建一个持久化存储供备份用:

# kubectl create -f - <<EOF
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mysql-backup
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: glusterfs-raid0
EOF

​ 继而,配置实际的自动化作业任务,如下所示,每天凌晨0点将使用mysqldump备份appdb数据库。

# kubectl create -f - <<EOF
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: mysql-backup
spec:
  schedule: "0 0 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: mysql-backup
            imagePullPolicy: IfNotPresent
            image: mysql
            env:
            - name: MYSQL_BACKUP_USER
              value: root
            - name: MYSQL_BACKUP_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-user-pwd
                  key: mysql-root-pwd
            - name: MYSQL_HOST
              value: mysql
            command:
            - /bin/sh
            - -c
            - |
              set -ex
              mysqldump --host=$MYSQL_HOST --user=$MYSQL_BACKUP_USER \
                        --password=$MYSQL_BACKUP_USER_PASSWORD \
                        --routines --databases appdb --single-transaction \
                        > /mysql-backup/mysql-`date +"%Y%m%d"`.sql
            volumeMounts:
            - name: mysql-backup
              mountPath: /mysql-backup
          restartPolicy: OnFailure
          volumes:
          - name: mysql-backup
            persistentVolumeClaim:
              claimName: mysql-backup
EOF

结束语

​ 本文揉合K8S多项技术,构建了一个复杂且可做生产使用的范例,当然,此库是单实例数据库,倘若需构建数据库高可用方案,需部署如MySQL HAPXC集群,其中自动作业备份范例仅使用mysqldump备份,在生产环境不是很实用,我们需要考虑使用xtrabackup备份以及mysqlbinlog备份日志。


  1. 参见docker-entrypoint.sh
查看原文

雪 赞了文章 · 2018-11-12

在K8S集群中一步步构建一个复杂的MySQL数据库

文档说明

​ 本文面向容器初学者,作者先简单的用MySQL官方镜像搭建一个可运行的单实例数据库,而后考虑生产或现实需求,一步一步完善并揉合K8S多个技术,从而构建一个复杂且可供生产用的MySQL单实例库。

简单部署

​ 如下所示,我们仅需设置root用户密码(环境变量MYSQL_ROOT_PASSWORD), 便可轻松的使用MySQL官方镜像构建一个MySQL数据库。

# kubectl create -f - <<EOF
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          value: Changeme
EOF

注意:若你的K8S集群是minishiftopenshiftorigin,因其为安全考虑,不允许容器以root用户运行,而官方MySQL镜像却需root权限,故为使其能顺利运行,我们需将anyuid scc赋予default serviceaccount

# oc adm policy add-scc-to-user anyuid -z default

​ 创建一Service以便集群内外均可访问数据库,其中集群外需通过nodePort设置的30006端口访问。

# kubectl create -f - <<EOF
apiVersion: v1
kind: Service
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  type: NodePort
  ports:
  - port: 3306
    nodePort: 30006
    protocol: TCP
    targetPort: 3306
  selector:
    app: mysql
EOF

​ 接着,访问数据库并验证其运行正常:

# kubectl get pod                                   # 当前Pod名称
NAME                     READY     STATUS    RESTARTS   AGE
mysql-5b5668c448-t44ml   1/1       Running   0          3h

# 通过本机访问
# kubectl exec -it mysql-5b5668c448-t44ml -- mysql -uroot -pChangeme

mysql> select 1;
+---+
| 1 |
+---+
| 1 |
+---+

# 集群内部通过mysql service访问:
# kubectl exec -it mysql-5b5668c448-t44ml -- mysql -uroot -pChangeme -hmysql

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2018-05-21 07:19:14 |
+---------------------+

# 集群外部,可通过任何一个K8S节点访问数据库:
# mysql -uroot -pChangeme -horigin-lb-01 -P30006

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+

扩展部署

持久化存储

​ 若要确保MySQL重启后数据仍然存在,我们需为其配置可持久化存储,作者的实验环境配置了GlusterFS分布式存储,其支持K8S动态提供特性,故可执行如下命令创建PVC

# kubectl create -f - <<EOF
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mysql
spec:
  accessModes:
    - ReadOnlyMany
  resources:
    requests:
      storage: 1Gi
  storageClassName: glusterfs-raid0
EOF

​ 而后,调整Deploy并挂载卷:

    spec:
      containers:
      - image: mysql
...
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql

自定义配置文件

​ 通过创建cm并挂载到容器中,我们可自定义MySQL配置文件。如下所示,名为mysql-configcm包含一个custom.cnf文件:

apiVersion: v1
metadata:
  name: mysql-config
data:
  custom.cnf: |
        [mysqld]
        default_storage_engine=innodb
        skip_external_locking
        lower_case_table_names=1
        skip_host_cache
        skip_name_resolve
kind: ConfigMap

​ 将cm挂载到容器内:

    spec:
...
      containers:
      - image: mysql
...
        volumeMounts:
        - name: mysql-config
          mountPath: /etc/mysql/conf.d/
...
      volumes:
      - name: mysql-config
        configMap:
          name: mysql-config
...

加密铭感数据

​ 用户密码等铭感数据以Secret加密保存,而后被Deployment通过volume挂载或环境变量引用。如本例,我们创建rootapptest用户,将3个用户的密码加密保存:

# echo -n Changeme | base64
Q2hhbmdlbWU=
# kubectl create -f - <<EOF
apiVersion: v1
kind: Secret
metadata:
  name: mysql-user-pwd
data:
  mysql-root-pwd: Q2hhbmdlbWU=
  mysql-app-user-pwd: Q2hhbmdlbWU=
  mysql-test-user-pwd: Q2hhbmdlbWU=
EOF

Secret创建完成后,我们将用户明文密码从Deployment去除,采用环境变量方式引用Secret数据,参见如下Yaml修改,做了3个调整:

  • 镜像初始化时自动创建MYSQL_DATABASE环境变量1设置的数据库;
  • 镜像初始化时将MYSQL_DATABASE数据库赋予MYSQL_USER用户;
  • root用户及MYSQL_USER用户,其密码均通过secretKeyRefsecret获取;
    spec:
...
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-root-pwd
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-app-user-pwd
        - name: MYSQL_USER
          value: app
        - name: MYSQL_DATABASE
          value: appdb

容器健康检查

K8S镜像控制器可通过livenessProbe判断容器是否异常,进而决定是否重建容器;而Service服务可通过readinessProbe判断容器服务是否正常,从而确保服务可用性。

​ 本例,作者配置的livenessProbereadinessProbe是一样的,即连续3次查询数据库失败,则定义为异常。对livenessProbereadinessProbe详细用法,不在本文的讨论范围内,可参考K8S官方文档:

    spec:
      containers:
      - image: mysql
...
        livenessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 30
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
...

容器初始化

假设,我们有这样的需求:“初始部署MySQL时,其已包应用所需的数据库、用户、权限、表结构与数据”。研究MySQL官方镜像的Dockerfile可知,数据库初始化时将自动执行目录/docker-entrypoint-initdb.d内的.sh.sql.sql.gz文件,鉴于此,我们可有如下两种方法:

  1. 基于官方镜像重新编写Dockerfile,将脚本copy到新镜像/docker-entrypoint-initdb.d目录,因需编译新镜像,故此方法不灵活;
  2. 初始化容器(initContainers)在常规容器(containers)前运行,故在初始化容器中可将脚本拷贝到共享目录,而后MySQL镜像挂载此目录到/docker-entrypoint-initdb.d,此方法灵活。

本例,作者采用初始化容器方案,功能如下:

  • 初始化与常规容器共享名为mysql-initdbemptyDir,均被挂载到/docker-entrypoint-initdb.d目录;
  • 初始化容器将.sql文件置于共享的/docker-entrypoint-initdb.d目录,其含初始化testdbappdb数据库;
  • 为了避免MySQL数据库目录内的lost+found目录被误认为是数据库,初始化容器中将其删除;
    spec:
      initContainers:
      - name: mysql-init
        image: busybox
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_TEST_USER_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-test-user-pwd
        command:  
          - sh
          - "-c"
          - |
            set -ex
            rm -fr /var/lib/mysql/lost+found
            cat > /docker-entrypoint-initdb.d/mysql-testdb-init.sql <<EOF
            create database testdb default character set utf8;
            grant all on testdb.* to 'test'@'%' identified by '$MYSQL_TEST_USER_PASSWORD';
            flush privileges;
            EOF
            cat > /docker-entrypoint-initdb.d/mysql-appdb-init.sql <<EOF
            create table app(id int);
            insert into app values(1);
            commit;
            EOF
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
      containers:
      - image: mysql
        volumeMounts:
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
...
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql
      - name: mysql-initdb
        emptyDir: {}
...

完整Deployment

​ 通过如上多步调整,MySQL数据库的Deplyment如下所示:


apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  labels:
    app: mysql
  name: mysql
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: mysql-init
        image: busybox
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_TEST_USER_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-test-user-pwd
        command:  
          - sh
          - "-c"
          - |
            set -ex
            rm -fr /var/lib/mysql/lost+found
            cat > /docker-entrypoint-initdb.d/mysql-testdb-init.sql <<EOF
            create database testdb default character set utf8;
            grant all on testdb.* to 'test'@'%' identified by '$MYSQL_TEST_USER_PASSWORD';
            flush privileges;
            EOF
            cat > /docker-entrypoint-initdb.d/mysql-appdb-init.sql <<EOF
            create table app(id int);
            insert into app values(1);
            commit;
            EOF
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
      containers:
      - image: mysql
        name: mysql
        imagePullPolicy: IfNotPresent
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-root-pwd
        - name: MYSQL_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-user-pwd
              key: mysql-app-user-pwd
        - name: MYSQL_USER
          value: app
        - name: MYSQL_DATABASE
          value: appdb
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: mysql-initdb
          mountPath: /docker-entrypoint-initdb.d
        - name: mysql-config
          mountPath: /etc/mysql/conf.d/
        ports:
        - name: mysql
          containerPort: 3306
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 30
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3
        readinessProbe:
          exec:
            command:
            - /bin/sh
            - "-c"
            - MYSQL_PWD="${MYSQL_ROOT_PASSWORD}"
            - mysql -h 127.0.0.1 -u root -e "SELECT 1"
          initialDelaySeconds: 10
          timeoutSeconds: 1
          successThreshold: 1
          failureThreshold: 3
      volumes:
      - name: mysql-data
        persistentVolumeClaim:
          claimName: mysql
      - name: mysql-initdb
        emptyDir: {}
      - name: mysql-config
        configMap:
          name: mysql-config

创建此Deployment后,我们有如下组件:

# kubectl get all,pvc,cm,secret
# MySQL Deployment:
NAME           DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deploy/mysql   1         1         1            1           1m

# RS被Deployment调用,其是自动生成的
NAME                 DESIRED   CURRENT   READY     AGE
rs/mysql-998977cdd   1         1         1         1m

# Pod:
NAME                       READY     STATUS    RESTARTS   AGE
po/mysql-998977cdd-v2ks2   1/1       Running   1          1m

# Service:
NAME              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
svc/mysql         NodePort    172.30.3.200    <none>        3306:30006/TCP   8h

# Pvc:
NAME            STATUS    VOLUME     CAPACITY   ACCESS MODES STORAGECLASS      AGE
pvc/mysql       Bound     pvc-fe..   1Gi        ROX          glusterfs-raid0   2m

# Configmap:
NAME              DATA      AGE
cm/mysql-config   1         6h

# Secret:
NAME                        TYPE                                  DATA      AGE
secrets/mysql-user-pwd      Opaque                                3         1h

定期自动备份

​ 考虑到数据安全性,我们定期备份数据库,在K8S集群中,我们可配置CronJob实现自动备份作业。首先,创建一个持久化存储供备份用:

# kubectl create -f - <<EOF
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: mysql-backup
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
  storageClassName: glusterfs-raid0
EOF

​ 继而,配置实际的自动化作业任务,如下所示,每天凌晨0点将使用mysqldump备份appdb数据库。

# kubectl create -f - <<EOF
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: mysql-backup
spec:
  schedule: "0 0 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: mysql-backup
            imagePullPolicy: IfNotPresent
            image: mysql
            env:
            - name: MYSQL_BACKUP_USER
              value: root
            - name: MYSQL_BACKUP_USER_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-user-pwd
                  key: mysql-root-pwd
            - name: MYSQL_HOST
              value: mysql
            command:
            - /bin/sh
            - -c
            - |
              set -ex
              mysqldump --host=$MYSQL_HOST --user=$MYSQL_BACKUP_USER \
                        --password=$MYSQL_BACKUP_USER_PASSWORD \
                        --routines --databases appdb --single-transaction \
                        > /mysql-backup/mysql-`date +"%Y%m%d"`.sql
            volumeMounts:
            - name: mysql-backup
              mountPath: /mysql-backup
          restartPolicy: OnFailure
          volumes:
          - name: mysql-backup
            persistentVolumeClaim:
              claimName: mysql-backup
EOF

结束语

​ 本文揉合K8S多项技术,构建了一个复杂且可做生产使用的范例,当然,此库是单实例数据库,倘若需构建数据库高可用方案,需部署如MySQL HAPXC集群,其中自动作业备份范例仅使用mysqldump备份,在生产环境不是很实用,我们需要考虑使用xtrabackup备份以及mysqlbinlog备份日志。


  1. 参见docker-entrypoint.sh
查看原文

赞 12 收藏 10 评论 5

雪 提出了问题 · 2018-10-17

解决fpm的dockerfile build问题

下面是我的dockerfile 文件

FROM php:7.1-fpm

RUN apt-get update -y \
    && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev zlib1g-dev libcurl3 libwebp-dev zlib1g \
    && docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ --with-webp-dir=/usr/include/ --with-png-dir=/usr/include \
    && docker-php-ext-install -j$(nproc) gd \
    ##
    && apt-get install -y libicu-dev \
    && docker-php-ext-install -j$(nproc) intl \
    ##
    && apt-get install -y libbz2-dev \
    && docker-php-ext-install bz2 \
    ##
    && apt-get install -y libxml2-dev \
    && apt-get install -y libxslt-dev \
    && docker-php-ext-install soap \
    && docker-php-ext-install xsl \
    && docker-php-ext-install xmlrpc \
    && docker-php-ext-install wddx \
    ##
    && docker-php-ext-install zip \
    && docker-php-ext-install pcntl \
    && docker-php-ext-install opcache \
    && docker-php-ext-install pdo \
    && docker-php-ext-install pdo_mysql \
    && docker-php-ext-install mysqli \
    && docker-php-ext-install mbstring \
    && docker-php-ext-install sockets \
    ##
    && pecl install redis-3.1.3 \
    && docker-php-ext-enable --ini-name pecl.ini redis  \
    ##
    && cd /tmp \
    && curl -L -s -o cphalcon.tar.gz https://github.com/phalcon/cphalcon/archive/v3.4.0.tar.gz \
    && tar xzvf cphalcon.tar.gz \
    && cd /tmp/cphalcon-3.4.0/build \
    && ./install \
    && echo "extension=phalcon.so" > /usr/local/etc/php/conf.d/phalcon.ini \
    ##
    && cd /tmp \
    && curl -L -s -o swoole.tar.gz https://github.com/swoole/swoole-src/archive/v2.1.0.tar.gz \
    && tar xzvf swoole.tar.gz \
    && cd /tmp/swoole-src-2.1.0 \
    && phpize \
    && ./configure \
    && make && make install \
    && echo "extension=swoole.so" > /usr/local/etc/php/conf.d/swoole.ini \
    ##
    && cd /tmp \
    && rm -rf cphalcon* \
    && rm -rf swoole* \
    && rm -rf package* 

EXPOSE 9000
CMD ["php-fpm"]

这样子build是没有问题的,但是因为里面有apt-get update 所以非常慢,因此google了下,说是用国内的源即可 然后变更为如下:

FROM php:7.1-fpm

COPY ./sources.list .

RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak && mv sources.list /etc/apt/ && apt-get update -y \
    && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libpng-dev zlib1g-dev libcurl3 libwebp-dev zlib1g \
    ......

sources.list 文件内容如下:

deb http://mirrors.163.com/debian/ jessie main non-free contrib
deb http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-updates main non-free contrib
deb-src http://mirrors.163.com/debian/ jessie-backports main non-free contrib
deb http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib
deb-src http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib

如果这样子build的话,就会报错:

The following information may help to resolve the situation:

The following packages have unmet dependencies:
 libfreetype6-dev : Depends: zlib1g-dev but it is not going to be installed or
                             libz-dev
 libpng12-dev : Depends: zlib1g-dev but it is not going to be installed
E: Unable to correct problems, you have held broken packages.

我在想是这个163的源有问题么?但是不应该啊,

求助下大家,这个问题怎么解决?

关注 2 回答 1

认证与成就

  • 获得 1 次点赞
  • 获得 26 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 21 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-01-20
个人主页被 199 人浏览