leeif

leeif 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/leeif 编辑
编辑

日本在职程序员
联系交流邮箱geeklyf@hotmail.com

个人动态

leeif 发布了文章 · 2019-09-13

[Golang] 聊一聊Go的那些处理命令行参数和配置文件的库

前言

最近应为一直在写Go,避免不了要处理一些命令行参数和配置文件。虽然Go原生的flag库比起其他语言,在处理命令行参数上已经做的很易用了,Go的社区也有很多好用的库。这篇文章主要介绍一下自己这段时间接触使用过库,为有同样需求的朋友也提供一些参考。

flag

首先还是有必要简单介绍一下Go的原生库flag, 直接上代码

基本用法

var id = flag.Int("id", 1, "user id")
var mail = flag.String("mail", "test@gmail.com", "mail")
var help = flag.Bool("h", false, "this help")

也可以用指针变量去接收flag

var name string
flag.StringVar(&name, "name", "leeif", "your name")

变量也可以是一个实现flag.Value接口的结构体

type Address struct {
    s string
}

func (a *Address) String() string {
    return a.s
}

func (a *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    a.s = s
    return nil
}

ad := Address{}
flag.Var(&ad, "address", "address of the server")

解析

flag.Parse()

完整代码
https://play.golang.org/p/mjgZ6SJMeAm

flagSet可以用来处理subcommand

upload := flag.NewFlagSet("upload", flag.ContinueOnError)
localFile := upload.Bool("localFile", false, "")
download := flag.NewFlagSet("download", flag.ContinueOnError)
remoteFile := download.Bool("remoteFile", false, "")

switch os.Args[1] {
  case "upload":
    if err := upload.Parse(os.Args[2:]); err == nil {
      fmt.Println("upload", *localFile)
    }
  case "download":
    if err := download.Parse(os.Args[2:]); err == nil {
      fmt.Println("download", *remoteFile)
    }
}

命令行的指定形式。

-flag (也可以是--flag)
-flag=x
-flag x  // non-boolean flags only

原生的flag在简单的需求下,已经够用了,但是想构建一些复杂的应用的时候还是有些不方便。然而flag的可扩展性也衍生了许多各具特色的第三方库。

kingpin

https://github.com/alecthomas...

一些主要的特点:

  • fluent-style的编程风格
  • 不仅可以解析flag, 也可以解析非flag参数
  • 支持短参数的形式
  • sub command

一般的使用方法

debug   = kingpin.Flag("debug", "Enable debug mode.").Bool()
// 可被环境变量覆盖的flag
// Short方法可以指定短参数
timeout = kingpin.Flag("timeout", "Timeout waiting for ping.").Default("5s").OverrideDefaultFromEnvar("PING_TIMEOUT").Short('t').Duration()
// IP类型的参数
// Required参数为必须指定的参数
ip      = kingpin.Arg("ip", "IP address to ping.").Required().IP()
count   = kingpin.Arg("count", "Number of packets to send").Int()

用指针类型接收flag

var test string
kingpin.Flag("test", "test flag").StringVar(&test)

实现kingpin.Value接口的参数类型

type Address struct {
    s string
}

func (a *Address) String() string {
    return a.s
}

func (a *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    a.s = s
    return nil
}

ad := Address{}
kingpin.Flag("address", "address of the server").SetValue

解析

kingpin.Parse()

使用sub command

var (
  deleteCommand     = kingpin.Command("delete", "Delete an object.")
  deleteUserCommand = deleteCommand.Command("user", "Delete a user.")
  deleteUserUIDFlag = deleteUserCommand.Flag("uid", "Delete user by UID rather than username.")
  deleteUserUsername = deleteUserCommand.Arg("username", "Username to delete.")
  deletePostCommand = deleteCommand.Command("post", "Delete a post.")
)

func main() {
  switch kingpin.Parse() {
  case deleteUserCommand.FullCommand():
  case deletePostCommand.FullCommand():
  }
}

kingpin会自动生成help文案。不用做任何设置用--help即可查看。-h则需要手动配置。

kingpin.HelpFlag.Short('h')

cobra

https://github.com/spf13/cobra
cobra是go程序员必须要知道的一款命令行参数库。很多大的项目都是用cobra搭建的。
cobra是为应用级的命令行工具而生的项目, 不仅提供了基本的命令行处理功能外, 而提供了一套搭建命令行工具的架构。

cobra的核型架构。

▾ appName/
    ▾ cmd/
        root.go
        sub.go
      main.go

所有的命令行配置分散写在各个文件中, 例如 root.go

package cmd

import (
    "fmt"
    "os"
    "github.com/spf13/cobra"
)

func init() {
  rootCmd.PersistentFlags().StringVarP(&projectBase, "projectbase", "b", "", "base project directory eg. github.com/spf13/")
}
var rootCmd = &cobra.Command{
  Use:   "hugo",
  Short: "Hugo is a very fast static site generator",
  Long: `A Fast and Flexible Static Site Generator built with
                love by spf13 and friends in Go.
                Complete documentation is available at http://hugo.spf13.com`,
  Run: func(cmd *cobra.Command, args []string) {
    // Do Stuff Here
  },
}

func Execute() {
  if err := rootCmd.Execute(); err != nil {
    fmt.Println(err)
    os.Exit(1)
  }
}

sub.go

package cmd

import (
  "fmt"

  "github.com/spf13/cobra"
)

func init() {
  rootCmd.AddCommand(subCmd)
}

var subCmd = &cobra.Command{
  Use:   "sub command",
  Short: "short description",
  Long:  `long description`,
  Run: func(cmd *cobra.Command, args []string) {
    fmt.Println("sub command")
  },
}

在最外面的main.go里,只用写一句话。

package main

import (
  "{pathToYourApp}/cmd"
)

func main() {
  cmd.Execute()
}

用cobra的架构来搭建命令行工具会使架构更清晰。

viper

https://github.com/spf13/viper
viper使用来专门处理配置文件的工具, 因为作者和cobra的作者是同一个人, 所以经常和cobra一起配合着使用。就连cobra的官方说明里也
提到了viper。

viper.SetConfigName("config") // name of config file (without extension)
viper.AddConfigPath("/etc/appname/")   // path to look for the config file in
viper.AddConfigPath("$HOME/.appname")  // call multiple times to add many search paths
viper.AddConfigPath(".")               // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
    panic(fmt.Errorf("Fatal error config file: %s \n", err))
}

获取读取到的参数, 为map[string]interface{}类型。

c := viper.AllSettings()

viper也提供处理flag的功能,但是个人感觉没有上面两个库好用,这里也就不做介绍了。

kiper

往往我们要同时处理命令行参数和配置文件, 并且我们想合并这两种参数。

虽然可以用cobra+viper可以实现, 但是个人喜欢kingpin, 因为kingpin可以检查参数的正确性(通过实现kingpin.Value接口的数据类型)。

于是自己写了一个kingpin+viper的wrapper工具, kiper。
https://github.com/leeif/kiper

主要特点:

  • 通过tag配置flag设定(kingpin)
  • 通过viper读取配置文件
  • 自动合并flag和配置文件参数

具体用法

package main

import (
    "errors"
    "fmt"
    "os"
    "strconv"

    "github.com/leeif/kiper"
)

type Server struct {
    Address *Address `kiper_value:"name:address"`
    Port    *Port    `kiper_value:"name:port"`
}

type Address struct {
    s string
}

func (address *Address) Set(s string) error {
    if s == "" {
        return errors.New("address can't be empty")
    }
    address.s = s
    return nil
}

func (address *Address) String() string {
    return address.s
}

type Port struct {
    p string
}

func (port *Port) Set(p string) error {
    if _, err := strconv.Atoi(p); err != nil {
        return errors.New("not a valid port value")
    }
    port.p = p
    return nil
}

func (port *Port) String() string {
    return port.p
}

type Config struct {
    ID     *int   `kiper_value:"name:id;required;default:1"`
    Server Server `kiper_config:"name:server"`
}

func main() {
    // initialize config struct
    c := &Config{
        Server: Server{
            Address: &Address{},
            Port:    &Port{},
        },
    }

    // new kiper
    k := kiper.NewKiper("example", "example of kiper")
    k.SetConfigFileFlag("config", "config file", "./config.json")
    k.Kingpin.HelpFlag.Short('h')

    // parse command line and config file
    if err := k.Parse(c, os.Args[1:]); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    fmt.Println(c.Server.Port)
    fmt.Println(*c.ID)
}

配置文件需要和Config结构体保持一致。

config.json

{
    "server": {
        "address": "192.0.0.1",
        "port": "8080"
    },
    "id": 2
}

有待改善的地方

  • 现在还没有做sub command的功能。
  • 合并的时候配置文件总会覆盖命令行参数(合并的优先顺序)

总结

Go社区给开发着提供了多种处理命令行参数和配置文件的工具。每种工具都有各自的特点和应用场景。 例如flag是原生支持,扩展性高。kingpin可以检查参数的正确性。cobra适合构建复杂的命令行工具。开发者可以根据自己搭需求选择使用的工具, 这样的可选性和自由度也正是Go社区最大的魅力。

查看原文

赞 2 收藏 1 评论 0

leeif 收藏了文章 · 2019-02-11

物联网高并发编程之网络编程中的线程模型

如需了解更多物联网网络编程知识请点击:物联网云端开发武器库


物联网高并发编程之网络编程中的线程模型

值得说明的是,具体选择线程还是进程,更多是与平台及编程语言相关。

例如 C 语言使用线程和进程都可以(例如 Nginx 使用进程,Memcached 使用线程),Java 语言一般使用线程(例如 Netty),为了描述方便,下面都使用线程来进行描述。

线程模型1:传统阻塞 I/O 服务模型

特点:

  • 1)采用阻塞式 I/O 模型获取输入数据;
  • 2)每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作。

存在问题:

  • 1)当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大;
  • 2)连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

线程模型2:Reactor 模式

基本介绍

针对传统阻塞 I/O 服务模型的 2 个缺点,比较常见的有如下解决方案: 

  • 1)基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理;
  • 2)基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。

Reactor 模式,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 

服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor 模式也叫 Dispatcher 模式。

即 I/O 多了复用统一监听事件,收到事件后分发(Dispatch 给某进程),是编写高性能网络服务器的必备技术之一。




Reactor 模式中有 2 个关键组成:

  • 1)Reactor:Reactor 在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对 IO 事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人;
  • 2)Handlers:处理程序执行 I/O 事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor 通过调度适当的处理程序来响应 I/O 事件,处理程序执行非阻塞操作。

根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:

  • 1)单 Reactor 单线程;
  • 2)单 Reactor 多线程;
  • 3)主从 Reactor 多线程。

单 Reactor 单线程

其中,Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求,其他方案示意图类似。

方案说明:

  • 1)Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
  • 2)如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理;
  • 3)如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
  • 4)Handler 会完成 Read→业务处理→Send 的完整业务流程。

优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成。
缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈。

可靠性问题,线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

使用场景:客户端的数量有限,业务处理非常快速,比如 Redis,业务处理的时间复杂度 O(1)。

单 Reactor 多线程

方案说明:

  • 1)Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发;
  • 2)如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后续的各种事件;
  • 3)如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应;
  • 4)Handler 只负责响应事件,不做具体业务处理,通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
  • 5)Worker 线程池会分配独立的线程完成真正的业务处理,将响应结果发给 Handler 进行处理;
  • 6)Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

优点:可以充分利用多核 CPU 的处理能力。
缺点:多线程数据共享和访问比较复杂;Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。

主从 Reactor 多线程

针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行。

方案说明:

  • 1)Reactor 主线程 MainReactor 对象通过 Select 监控建立连接事件,收到事件后通过 Acceptor 接收,处理建立连接事件;
  • 2)Acceptor 处理建立连接事件后,MainReactor 将连接分配 Reactor 子线程给 SubReactor 进行处理;
  • 3)SubReactor 将连接加入连接队列进行监听,并创建一个 Handler 用于处理各种连接事件;
  • 4)当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应;
  • 5)Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理;
  • 6)Worker 线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给 Handler 进行处理;
  • 7)Handler 收到响应结果后通过 Send 将响应结果返回给 Client。

优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。

父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。

这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

小结

3 种模式可以用个比喻来理解:(餐厅常常雇佣接待员负责迎接顾客,当顾客入坐后,侍应生专门为这张桌子服务)

  • 1)单 Reactor 单线程,接待员和侍应生是同一个人,全程为顾客服务;
  • 2)单 Reactor 多线程,1 个接待员,多个侍应生,接待员只负责接待;
  • 3)主从 Reactor 多线程,多个接待员,多个侍应生。

Reactor 模式具有如下的优点:

  • 1)响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
  • 2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  • 3)可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
  • 4)可复用性,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性。

线程模型2:Proactor 模型

在 Reactor 模式中,Reactor 等待某个事件或者可应用或者操作的状态发生(比如文件描述符可读写,或者是 Socket 可读写)。

然后把这个事件传给事先注册的 Handler(事件处理函数或者回调函数),由后者来做实际的读写操作。

其中的读写操作都需要应用程序同步操作,所以 Reactor 是非阻塞同步网络模型。

如果把 I/O 操作改为异步,即交给操作系统来完成就能进一步提升性能,这就是异步网络模型 Proactor。

Proactor 是和异步 I/O 相关的,详细方案如下:

  • 1)Proactor Initiator 创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 AsyOptProcessor(Asynchronous Operation Processor)注册到内核;
  • 2)AsyOptProcessor 处理注册请求,并处理 I/O 操作;
  • 3)AsyOptProcessor 完成 I/O 操作后通知 Proactor;
  • 4)Proactor 根据不同的事件类型回调不同的 Handler 进行业务处理;
  • 5)Handler 完成业务处理。

可以看出 Proactor 和 Reactor 的区别:

  • 1)Reactor 是在事件发生时就通知事先注册的事件(读写在应用程序线程中处理完成);
  • 2)Proactor 是在事件发生时基于异步 I/O 完成读写操作(由内核完成),待 I/O 操作完成后才回调应用程序的处理器来进行业务处理。

理论上 Proactor 比 Reactor 效率更高,异步 I/O 更加充分发挥 DMA(Direct Memory Access,直接内存存取)的优势。

但是Proactor有如下缺点: 

  • 1)编程复杂性,由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以 Debug;
  • 2)内存使用,缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比 Reactor 模式,在 Socket 已经准备好读或写前,是不要求开辟缓存的;
  • 3)操作系统支持,Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux 2.6 才引入,目前异步 I/O 还不完善。

因此在 Linux 下实现高并发网络编程都是以 Reactor 模型为主。

参考:http://www.52im.net/forum.php

查看原文

leeif 赞了文章 · 2019-02-03

前端小报 - 201901 月刊

订阅 / 投稿:https://github.com/txd-team/monthly
本期小编:x-cold (尹挚)

新闻快报

重磅消息:Github 宣布私有仓库免费,同期还上线了星标话题 (topics) 的功能

2019 年伊始,GitHub 正式宣布开放无限制创建私有存储库,并开始提供统一的企业版 GitHub 服务,微软正在开始加速 toB 企业服务的布局。开发者们一片叫好声,可谓年度最佳的程序员福利了。

软件技术 B2B 公司 Idera.inc 收购 Travis CI

Travis CI 将结合 Idera 公司其本身的数据库 / 开发者 / 测试等工具发挥更大的商业优势。不过使用免费版的社区同学也不用紧张,Travis CI 依然保证对开源软件免费的策略,并且其开源证书不会修改 (MIT),更多可以参考详细的官方公告

需要额外提醒的是, Travis-CI 对 Github 私有仓库依然没有提供免费的服务。

ES2019 即将到来,新特性快速一览

image.png

越来越多的项目开始迁移到 TypeScript

MemSQL Studio 的 3W+ 行前端源代码从 Flow 迁移到 TypeScript,主要目的是加强类型的控制,避免动态/弱类型造成运行时的各种问题。官方总结了一篇文章,介绍了他们为什么要迁移到 TypeScript,以及迁移过程的记录。

Fackbook 开源的测试框架 Jest 通过几番的讨论终于从 JavaScript 迁移到使用 TypeScript 构建整个项目了,详细的过程可以查看关联的 Pull Request

1 月 25 日,Yarn 官方团队在 GitHub issue 中宣布将对 Yarn 进行重大更改,主要包括:将代码库从 Flow 移植到 TypeScript,不再支持 Node 4 和 Node 6,并为 Yarn 新增了一些功能等,这个更改项目代号为 Berry,目的是增强 Yarn 优势,弥补弱势。(本条消息来自 InfoQ)

Midway v1.0 社区

Midway 是一款基于 Egg 和 TypeScript 的 Web 开发框架,由来自淘宝的 MidwayJS 团队开发。用户可以使用丰富的装饰器快速开发,并且引入了 IoC 的概念,统一管理依赖和统一初始化,是一款面向未来的全栈开发方案。

JavaScript 成为 2018 年最受欢迎的编程语言

随着前端领域的迅猛发展,JavaScript 语言已经延伸到更丰富的使用场景中,HackerRank 在社区发起了 2018 年度的开发者调查,其中就包含了编程语言和框架。调查结果显示:JavaScript 在 2018 年度荣获最受欢迎的编程语言。

简要讯息

Github Trending

洞察 Github 近期 Hot Fresh Repository

flutter 开发者帮助 APP,包含 flutter 常用 140+ 组件的demo 演示与中文文档,帮助开发者快速上手 Flutter 内部测试中,1.0 正式版将于 2月 20日 发布。

Trilium Notes 是一款知识库构建工具,支持无限层级扩展,文档可以挂载到多个节点上,采用所见即所得的编辑方式。体验上个人感觉有点像桌面版本的语雀。

国服第一切图仔 chokcoco 整理的各种 CSS 技巧,帮助学习 CSS 和寻找灵感,以分类的形式,展示不同 CSS 属性或者不同的课题使用 CSS 来解决的各种方法。[](https://github.com/chokcoco/C...

一款高颜值的流媒体资源音乐🎵播放器,支持网络上所有免费的流媒体资源。

一款“带给我惊喜”的 vscode 插件,提供给给编辑器实时预览页面的能力,可以将 vscode 打造成更强大的集成开发环境 (IDE) 了。

1548992494875-b282cb53-c06d-40f1-ae4c-d142837e5377.gif

精品学习

TypScript 学习指导

TypeScript 最近可谓如火如,它是 JavaScript 的超集,其最大的特点就是支持了类型系统。其火爆的中最核心的推动力无非是前端工程规模的增长,覆盖端 (服务端 node / deno、移动端等)的扩展,学习和使用 TypeScript 能够让我们更轻松地应付一些复杂的开发场景。

「墨者修齐」数据可视化周刊

精选文章推荐,可视化入门与进阶权威网站、论文、工具介绍,工程与设计实践分享,2019 年起每周一更新。

Development 技术播客列表

涵盖了开发相关的方方面面,包括编程语言、AI、Devops、Web 开发等丰富的课程体系。

Flutter-learning

Flutter-learning 整理了 Flutter 相关学习资料,包括 Flutter安装和配置,Flutter开发遇到的难题,Flutter示例代码和模板,Flutter项目实战,Dart语言学习示例代码。

工具推荐

一款可以随手验证你的想法的桌面应用程序,输入 JS 代码片段既可预览执行结果,支持最新的 ES 特性。(PS: 也可以用来当做代码片段备忘录)

在线生成 image maps 的可视化工具,如果你想要绘制简单的局部可交互地图、编写邮件时希望添加链接到图片指定区块上,不妨通过 image maps 这项古老悠久的技术来实现,image-map 是一款在线进行可视化编辑,生成对应的 map / area 代码的工具。

mjml 是一种用于创建响应式的邮件的标记语言,通过编写语义化的标签,会自动帮助你转换成标准的 html 的代码,并且使用了 table 进行布局,非常适合编写富文本邮件。如果有这方面的需求,不妨进一步试试桌面版的 mjml-app 吧。

支持 windows,linux,macos 三端的命令行工具,集成的SSH客户端和连接管理器,可定制化程度非常高。

其他

最近在 Reddit 看到的前端 vs 后端的一些对比图,“很是真实”,献上给大家。

各位客官猪年大吉呀🌺🌺🌺,祝福新一年里猪事顺利!!! 另外 2020 年毕业的同学别忘了准备阿里巴巴春季实习生招聘哈,需要内推的同学也可以提前发送附件简历到我的邮箱。

附录

查看原文

赞 24 收藏 15 评论 0

leeif 收藏了文章 · 2019-02-02

12个令人惊叹的CSS实验项目

翻译:疯狂的技术宅
原文:https://1stwebdesigner.com/12...

本文首发微信公众号:jingchengyideng
欢迎关注,每天都给你推送新鲜的前端技术文章


你可能认为 CSS 只是一种简单地为网页设计样式的语言,但它的功能比你想象的要多得多。 从逼真的图像到甚至是视频游戏,你会惊讶地看到一个优秀的开发者可以用 CSS 做些什么。

这里有各种滤镜和特效,它们都是开源的,可以用在你自己的 web 项目中。 这些模块有的机遇 JavaScript,更多的是HTML。 这意味着它们比你期望的更轻盈。 看看这些惊人的纯CSS实验,也许你自己也可以尝试一下。

太阳系

clipboard.png
哇! 如果你喜欢太空,一定会被这个用 CSS 实现的的太阳系动画效果所震撼。 这不仅仅是一个漂亮的动画; 相对于真实的地球年,每个行星都能准确地围绕太阳旋转。

查看演示

项目链接:https://codepen.io/kowlor/pen...

渐变背景动画效果

clipboard.png

动画对于网站来说是一个臭名昭着的问题。如果优化不佳,可能会导致速度大服务放缓。这个美丽的动画渐变效果非常轻巧,更不用说它能让你很容易的就能编辑和添加自己的颜色。

项目链接:https://codepen.io/P1N2O/pen/...

叠叠高游戏

clipboard.png

你可以不用 JavaScript 来编写一个游戏。这个纯粹用 CSS 实现的叠叠高游戏看上去很简单,但是很有趣,而且图形也很漂亮。虽然做出来并不容易,但这只也仅仅是让 CSS 小小的露了一手。

查看演示

项目链接:https://codepen.io/finnhvman/...

3D进度条

clipboard.png

漂亮轻便的进度条。易于定制,很容易适应你的项目。 这些条纹使用 3D 技术制作,具有独特的液体外观。 你甚至可以将它们变成迷你 3D 图表!

查看演示

项目链接:https://codepen.io/rgg/pen/Qb...

出故障的文字

clipboard.png

故障文本看起来总是很酷。这个案例没有使用 GIF,仅用 JavaScript 或 HTML 就实现了生动的特效。 如果你想为你的网站添加小故障效果,请参考它。

查看演示

项目链接:https://codepen.io/lbebber/pe...

Francine

Francine

你可以用 HTML 和 CSS 制作艺术品! Francine 是一副18世纪风格的画作,纯粹用代码制作和展示。 然而它看起来与其他传统创作的艺术品没有任何区别。

项目链接:https://github.com/cyanharlow...

手机

clipboard.png

与 Francine 类似,这款手机也是只用 CSS 和 HTML 创造的,但是看上去简直和真的一样! 如果你有兴趣,可以使用代码并查看如何实现。

查看演示

项目链接:https://codepen.io/Wujek_Greg...

地图创作器

clipboard.png

你以为要用 JavaScript 来编写这东西? 再好好想想! 这个可爱的 3D 地图创作器除了 CSS(还有一点点HTML)之外什么都没有。 难道这不足以令人兴奋吗?

查看演示

项目链接:https://codepen.io/onediv/pen...

Instagram.css

clipboard.png

你的网站需要一些仿 Instagram 风格的过滤器? 这组缩小文件也附带安装教程。 现在,你可以轻松地将 Instagram 过滤器添加到任何图像中。

项目链接:https://picturepan2.github.io...

鬼影渐变效果按钮

clipboard.png

令人惊讶的是它是只用 CSS 编写的。 凭借其漂亮的动画和渐变效果,把这个按钮用在任何网站上,看起来都会很棒。

查看演示

项目地址:https://codepen.io/ARS/pen/vE...

Devices.css

clipboard.png

如果你曾经想在自己的网站上展示手机或电脑,并在屏幕上显示你所选择的图片,请参考此项目。 这些都是以现代设备为蓝本设计的!

项目地址:https://picturepan2.github.io...

动态图像着色

clipboard.png

这是一个非常酷的项目:用 CSS 和颜色选择工具更改图片中的颜色。

演示效果

项目地址:https://codepen.io/noahblon/p...

小巧、灵敏和美丽

你在网站上看到的许多惊人的特效都可以说是 JavaScript 的功劳,遗憾的是 JS 并不总是最轻量级的解决方案。 不过你可能会对CSS的功能感到惊讶。 如果做得正确,大多数情况下它对性能的影响要小得多。

无论是哪种方式,可以看到 CSS开发者提出的这些创意都很有趣。这些实验项目是由一些真正的创新开发者做出的,所以请去给他们一些支持,并告诉我你觉得哪个是最酷的!


本文首发微信公众号:jingchengyideng

欢迎扫描二维码关注公众号,每天推送我翻译的技术文章

欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章


查看原文

leeif 收藏了文章 · 2019-02-02

九种跨域方式实现原理(完整版)

前言

前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。

本文完整的源代码请猛戳github博客,纸上得来终觉浅,建议动手敲敲代码

一、什么是跨域?

1.什么是同源策略及其限制内容?

同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。
url的组成
同源策略限制内容有:

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点
  • AJAX 请求发送后,结果被浏览器拦截了

但是有三个标签是允许跨域加载资源:

  • <img data-original=XXX>
  • <link href=XXX>
  • <script data-original=XXX>

2.常见跨域场景

当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示:

特别说明两点:

第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。

第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”

这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。

二、跨域解决方案

1.jsonp

1) JSONP原理

利用 <script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。

2) JSONP和AJAX对比

JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)

3) JSONP优缺点

JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。

4) JSONP的实现流程

  • 声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。
  • 创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。
  • 服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show('我不爱你')
  • 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})

上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show('我不爱你'),最后会运行show()这个函数,打印出'我不爱你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不爱你')`)
})
app.listen(3000)

5) jQuery的jsonp形式

JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。

$.ajax({
url:"http://crossdomain.com/jsonServerResponse",
dataType:"jsonp",
type:"get",//可以省略
jsonpCallback:"show",//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略
jsonp:"callback",//->把传递函数名的那个形参callback,可省略
success:function (data){
console.log(data);}
});

2.cors

CORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现

浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。

虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求复杂请求

1) 简单请求

只要同时满足以下两大条件,就属于简单请求

条件1:使用下列方法之一:

  • GET
  • HEAD
  • POST

条件2:Content-Type 的值仅限于下列三者之一:

  • text/plain
  • multipart/form-data
  • application/x-www-form-urlencoded

请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。

2) 复杂请求

不符合以上条件的请求就肯定是复杂请求了。
复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。

我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:

// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// OPTIONS请求不做任何处理
if (req.method === 'OPTIONS') {
  res.end() 
}
// 定义后台返回的内容
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})

接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段

// index.html
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiamen' // cookie不能跨域
xhr.withCredentials = true // 前端设置是否带cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiamen')
xhr.onreadystatechange = function() {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      console.log(xhr.response)
      //得到响应头,后台需设置Access-Control-Expose-Headers
      console.log(xhr.getResponseHeader('name'))
    }
  }
}
xhr.send()
//server1.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000);
//server2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000'] //设置白名单
app.use(function(req, res, next) {
  let origin = req.headers.origin
  if (whitList.includes(origin)) {
    // 设置哪个源可以访问我
    res.setHeader('Access-Control-Allow-Origin', origin)
    // 允许携带哪个头访问我
    res.setHeader('Access-Control-Allow-Headers', 'name')
    // 允许哪个方法访问我
    res.setHeader('Access-Control-Allow-Methods', 'PUT')
    // 允许携带cookie
    res.setHeader('Access-Control-Allow-Credentials', true)
    // 预检的存活时间
    res.setHeader('Access-Control-Max-Age', 6)
    // 允许返回的头
    res.setHeader('Access-Control-Expose-Headers', 'name')
    if (req.method === 'OPTIONS') {
      res.end() // OPTIONS请求不做任何处理
    }
  }
  next()
})
app.put('/getData', function(req, res) {
  console.log(req.headers)
  res.setHeader('name', 'jw') //返回一个响应头,后台需设置
  res.end('我不爱你')
})
app.get('/getData', function(req, res) {
  console.log(req.headers)
  res.end('我不爱你')
})
app.use(express.static(__dirname))
app.listen(4000)

上述代码由http://localhost:3000/index.htmlhttp://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。

3.postMessage

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递

postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • message: 将要发送到其他 window的数据。
  • targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
  • transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。

接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回"我不爱你"。

// a.html
  <iframe data-original="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件
  //内嵌在http://localhost:3000/a.html
    <script>
      function load() {
        let frame = document.getElementById('frame')
        frame.contentWindow.postMessage('我爱你', 'http://localhost:4000') //发送数据
        window.onmessage = function(e) { //接受返回数据
          console.log(e.data) //我不爱你
        }
      }
    </script>
// b.html
  window.onmessage = function(e) {
    console.log(e.data) //我爱你
    e.source.postMessage('我不爱你', e.origin)
 }

4.websocket

Websocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

我们先来看个例子:本地文件socket.html向localhost:3000发生数据和接受数据

// socket.html
<script>
    let socket = new WebSocket('ws://localhost:3000');
    socket.onopen = function () {
      socket.send('我爱你');//向服务器发送数据
    }
    socket.onmessage = function (e) {
      console.log(e.data);//接收服务器返回的数据
    }
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws');//记得安装ws
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
  ws.on('message', function (data) {
    console.log(data);
    ws.send('我不爱你')
  });
})

5. Node中间件代理(两次跨域)

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:

  • 接受客户端请求 。
  • 将请求 转发给服务器。
  • 拿到服务器 响应 数据。
  • 将 响应 转发给客户端。

我们先来看个例子:本地文件index.html文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据。

// index.html(http://127.0.0.1:5500)
 <script data-original="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
    <script>
      $.ajax({
        url: 'http://localhost:3000',
        type: 'post',
        data: { name: 'xiamen', password: '123456' },
        contentType: 'application/json;charset=utf-8',
        success: function(result) {
          console.log(result) // {"title":"fontend","password":"123456"}
        },
        error: function(msg) {
          console.log(msg)
        }
      })
     </script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
  // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段
  response.writeHead(200, {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': '*',
    'Access-Control-Allow-Headers': 'Content-Type'
  })
  // 第二步:将请求转发给服务器
  const proxyRequest = http
    .request(
      {
        host: '127.0.0.1',
        port: 4000,
        url: '/',
        method: request.method,
        headers: request.headers
      },
      serverResponse => {
        // 第三步:收到服务器的响应
        var body = ''
        serverResponse.on('data', chunk => {
          body += chunk
        })
        serverResponse.on('end', () => {
          console.log('The data is ' + body)
          // 第四步:将响应结果转发给浏览器
          response.end(body)
        })
      }
    )
    .end()
})
server.listen(3000, () => {
  console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
  if (request.url === '/') {
    response.end(JSON.stringify(data))
  }
})
server.listen(4000, () => {
  console.log('The server is running at http://localhost:4000')
})

上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在index.html文件打印出{"title":"fontend","password":"123456"}

6.nginx反向代理

实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

先下载nginx,然后将nginx目录下的nginx.conf修改如下:

// proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;
    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

最后通过命令行nginx -s reload启动nginx

// index.html
var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
// server.js
var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });
    res.write(JSON.stringify(params));
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

7.window.name + iframe

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html(http://localhost:3000/b.html)
  <iframe data-original="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
  <script>
    let first = true
    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    function load() {
      if(first){
      // 第1次onload(跨域页)成功后,切换到同域代理页面
        let iframe = document.getElementById('iframe');
        iframe.src = 'http://localhost:3000/b.html';
        first = false;
      }else{
      // 第2次onload(同域b.html页)成功后,读取同域window.name中数据
        console.log(iframe.contentWindow.name);
      }
    }
  </script>

b.html为中间代理页,与a.html同域,内容为空。

 // c.html(http://localhost:4000/c.html)
  <script>
    window.name = '我不爱你'  
  </script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

8.location.hash + iframe

实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。
同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000

 // a.html
  <iframe data-original="http://localhost:4000/c.html#iloveyou"></iframe>
  <script>
    window.onhashchange = function () { //检测hash的变化
      console.log(location.hash);
    }
  </script>
 // b.html
  <script>
    window.parent.parent.location.hash = location.hash 
    //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面
  </script>
 // c.html
 console.log(location.hash);
  let iframe = document.createElement('iframe');
  iframe.src = 'http://localhost:3000/b.html#idontloveyou';
  document.body.appendChild(iframe);

9.document.domain + iframe

该方式只能用于二级域名相同的情况下,比如 a.test.comb.test.com 适用于该方式
只需要给页面添加 document.domain ='test.com' 表示二级域名都相同就可以实现跨域。

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值

// a.html
<body>
 helloa
  <iframe data-original="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
  <script>
    document.domain = 'zf1.cn'
    function load() {
      console.log(frame.contentWindow.a);
    }
  </script>
</body>
// b.html
<body>
   hellob
   <script>
     document.domain = 'zf1.cn'
     var a = 100;
   </script>
</body>

三、总结

  • CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案
  • JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
  • 不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。
  • 日常工作中,用得比较多的跨域方案是cors和nginx反向代理

给大家推荐一个好用的BUG监控工具Fundebug,欢迎免费试用!

参考文章

查看原文

leeif 发布了文章 · 2019-01-20

[MySQL进阶之路][No.0003] 主从复制(Replication)在运维中的运用

前序

在之前的两章里,主要介绍的主从复制(Replication)的基本使用方法和show slave status一些基本参数,这一章我们讲一下主从复制在生产环境中的一些在运维中的常见用途。下面将要介绍的一些运用可能在不同的地方有不同的叫法, 我取的名字可能比较随意, 如果有不准确或者有歧义的地方还请多多指教。

整体迁移

首先最常见的是主从数据库整体的迁移。具体的运用场景例如,当要换一批新的服务器的时候,需要将当前旧服务器的数据库服务迁移到新的服务器上。这个时候就可以利用主从复制来进行服务器的更换。具体流程如下图。
图片描述

图例很简单,把新的主从集群做为slave挂在原来的集群下。之后把写入读取的指向改到新服务器上,整个迁移工作也就算完成了。
需要注意的是new master中一定要设置log_slave_updates为有效,不然不会将更新写入binlog文件中。

整体合并

合并两个或者多个主从数据库群也是在运维中经常见到的。为了节省成本,需要将多个主从集群合并到一个集群中,这时可以运用replication来整合数据。从多个主服务器获取数据需要运用multi-source replication(MSR)。其实就是通过change master命令的channel参数来区分多个复制源。

图片描述

大体的原理如上图所示, 这里需要注意的是如果你使用的是5.7, 并且设置了binlog_format=row, binlog_rows_query_log_events=on。
使用MSR会出现memory leak的bug。所以在5.7版本下,把binlog_format设成statement之后再做MSR吧。
https://bugs.mysql.com/bug.ph...

数据表分割

当一张表越来越到大的时候, 其查询速度也会变的越来越慢。 为了优化性能, 我们需要对数据表进行分割。数据表分割分为垂直分割和水平分割。
垂直分割是对表的列(column)进行分割。如果一张表里有一些列不经常用,可以用这种分割方法来提高搜索速度。缺点就是提高了业务层面的复杂度。
水平分割是将一张数据表的数据(record)分散到多个表里。这样不仅可以减小索引B+树的层数,减少磁盘的读取次数提高索引速度。(只有当索引是比较大的值的时候优化提升比较显著)

水平分割和垂直分割的实现方法可能有很多种, 用replication来实现数据表分割个人觉得理解和实现比较简单。

水平分割

图片描述

例如上面的例子,本来的数据库里Table1表里的user1和user2的两个数据,通过replication将数据复制到两个新的主从集群中,再将不要的数据删除, 简单的水平分割就完成了。

垂直分割

和水平分割一样,只是在删除数据的时候删除column。

小结

简单介绍了几种比较常见的replication运用的实例。这里只是介绍了最基本的实现原理,在实际生产环境中需要注意的东西还有很多。在以后有机会再同大家分享。

查看原文

赞 4 收藏 4 评论 0

leeif 赞了回答 · 2018-11-05

解决找出其他相似的文章?A,B,C.......

我建议 tags 分出来一个表,然后加一个中间表维护文章和 tags 的关系,这样查询和后期维护都会更好

关注 5 回答 4

leeif 发布了文章 · 2018-09-29

[MySQL进阶之路][No.0002] SHOW SLAVE STATUS

进入正题之前

上回我们聊到关于MySQL的replication。https://segmentfault.com/a/11... 在进入今天的正题之前,先说一个可能大家很容易踩的坑。

Authentication Plugins问题

上回中实践部分用的是MySQL5.7。当你要把MySQL升级到MySQL8.0的时候,如果用同样的方法change master然后start slave后查看slave的状态的时候可能会发现以下错误。

mysql> show slave status\G
...
Last_IO_Error: error connecting to master 'root@10.1.0.102:3306' - retry-time: 60  retries: 1
...

slave没有脸上master,这时候你的第一反应可能是change master写错了或者master的MySQL挂掉了等等。如果你的配置文件里没有default_authentication_plugin这个参数,那多半是在它那跌倒了。
在MySQL5.7时,default_authentication_plugin的默认值是mysql_native_password。而到了MySQL8.0后,默认值变成了caching_sha2_password,也就是说现在的密码默认被加密了,当然用以前的方法是连不上master服务器的了。下面两个方法仅供参考。

解决方法一

如果你之前一直使用mysql_native_password,并且很多工具都是在此之上写出来的。这样的话建议在MySQL8.0中继续使用, 只要在在master的配置文件my.cnf中设置default_authentication_plugin = mysql_native_password就行了。

解决方法二

如果你想使用caching_sha2_password,master和slave必须同时设置成支持加密链接。首先必须在master和slave中设置ssl连接。
创建master和slave的配对证书。(OpenSSL)

//自签名一个ca
$ openssl genrsa 2048 > ca-key.pem
$ openssl req -new -x509 -nodes -days 3600 -key ca-key.pem -out ca.pem

//用上面的ca签一个服务端证书
$ openssl req -newkey rsa:2048 -days 3600 -nodes -keyout server-key.pem -out server-req.pem

$ openssl rsa -in server-key.pem -out server-key.pem
$ openssl x509 -req -in server-req.pem -days 3600 -CA ca.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem

//用上面的ca签一个客户端证书
$ openssl req -newkey rsa:2048 -days 3600 -nodes -keyout client-key.pem -out client-req.pem
$ openssl rsa -in client-key.pem -out client-key.pem
$ openssl x509 -req -in client-req.pem -days 3600 -CA ca.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem

master和slave的配置文件中加入

//my.cnf
...
[mysqld]
ssl-ca=ca.pem
ssl-cert=server-cert.pem
ssl-key=server-key.pem
...
[client]
ssl-ca=ca.pem
ssl-cert=client-cert.pem
ssl-key=client-key.pem
...

slave change master, 将MASTER_SSL设成1。replication成功。

mysql> change master to
    -> MASTER_HOST = '10.1.0.102',
    -> MASTER_USER = 'root',
    -> MASTER_LOG_FILE = 'binlog.000002',
    -> MASTER_LOG_POS = 154,
    -> MASTER_SSL = 1;
Query OK, 0 rows affected, 1 warning (0.06 sec)

mysql> start slave;

设置ssl还是很麻烦的,官方也提供了一个脚本方便大家设置ssl连接。https://dev.mysql.com/doc/ref...

SHOW SLAVE STATUS

今天将介绍的主角是show slave status这个命令,

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.1.0.102
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: binlog.000002
          Read_Master_Log_Pos: 155
               Relay_Log_File: 29d3afe99c90-relay-bin.000002
                Relay_Log_Pos: 319
        Relay_Master_Log_File: binlog.000002
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 155
              Relay_Log_Space: 534
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: Yes
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 11
                  Master_UUID: c41449f4-c3a3-11e8-b5c6-02420a010066
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
       Master_public_key_path:
        Get_master_public_key: 0
1 row in set (0.00 sec)

mysql>

这个命令也就是输出一些slave的命令。为什么说这个命令重要,首先通过这个命令可以实时掌握replication的状况,而是在实际的运营中,replication出了问题通过这个命令也可以第一时间定位问题。了解这个命令中各参数的含义对于主从关系的MySQL运营维护有这重要的实际意义。

Slave_IO_State

当前slave的IO线程的状况。是show processlist里IO线程state的复制。
Waiting for master to send event
slave正在等待master更新。如果等待时间超过slave_net_timeout(my.cnf设置),IO线程为重连master。

Waiting for the slave SQL thread to free enough relay log space
如果你设置了relay_log_space_limit,当relay log大小超过这个值以后,IO线程会先等SQL线程删掉一部分relay log。

全部的state,https://dev.mysql.com/doc/ref...

MASTER_HOST,MASTER_USER,MASTER_PORT

master的地址,连接用户名,端口等的信息。

Connect_Retry

当master和slave之前出现连接问题时,每隔这个时间就会尝试一次重连master。可用过change master设置这个值。

Master_Log_File, Relay_Log_File, Relay_Master_Log_File

Master_Log_File
当前IO从master读取的binlog的文件名。
Relog_Log_File
slave的SQL先前当读取的relay log文件名。
Relay_Master_log_File
当前SQL执行的最新的SQL Event是包含在master哪个binlog文件中的。

Read_Master_Log_Pos, Relay_Log_Pos, Exec_Master_Log_Pos

这三个参数可以说是至关重要,也经常被搞混。
Read_Master_Log_Pos
I/O读取到的log在master的binlog中的位置。

Relay_Log_Pos
SQL执行到的Relay Log的位置。

Exec_Master_Log_Pos
SQL执行到的SQL Event在master的binlog中的位置。

如果Read_Master_Log_Pos和master的show master status的位置一样,而Exec_Master_Log_Pos的值小于它们,那说明SQL线程出现了过载,正在执行一个非常熬时间的SQL或者slave服务器的性能出现恶化等等。

Slave_IO_Running, Slave_SQL_Running

IO线程,SQL线程是否在运行。
Slave_IO_Running = NO,IO线程没运行。
Slave_IO_Running = Connecting, IO线程正在运行,但是没连上master。
Slave_IO_Running = YES,IO线程在运行,并且连上了master。

Relay_Log_Space

Relay log的全部加起来的大小

Last_Errno, Last_Error

SQL线程上次的执行错误信息

Master_SSL_*

SSL连接时的设定。开头的ssl连接的例子中,如果我们没在slave中my.cnf设置证明书信息,我们需要通过change master手动设置。

Replicate_Do_DB, Replicate_Ignore_DB, Replicate_Do_Table, Replicate_Ignore_Table, Replicate_Wild_Do_Table, Replicate_Wild_Ignore_Table

Replication可以通过在my.cnf中设置--replicate-do-table等来设定具体同步哪些库或表。

Seconds_Behind_Master

简单来说,就是slave比master慢了多少。如果slave比master慢了很多,读写分离的架构中,用户读取到的数据就不是最新的,运营事故就很容易发生。一般情况下,如果这个值过大,我们可以考虑是否是slave服务器SQL线程出问题了。
如果对这个值是怎么计算出来的感兴趣的通许可以看一下这篇文章。
http://mysql.taobao.org/month...
需要记得一点的就是,当网络状况很差的时候,这个值会一直是0。所以只有在网络环境很好的前提下,这个值才能表示slave比master慢多少。所以这个参数有时候也并不可靠。

Channel_name

channel是指我们可以在slave同时指定多个master进行replication,用不同的channel名来进行区分。这个也叫multi-source replication。再合并多个master的时候很有用。实际操作也只要在change master to后面加上for channel字段就可以了
。同样start/stop slave后面也加上for channel就行了。

总结

今天就讲到这,上面只是介绍了一部分可能会场用到的参数,想了解其他参数的可以看官方文档。https://dev.mysql.com/doc/ref...
下章讲一下一些在运营中常见的一些主从切换的方法。

查看原文

赞 5 收藏 4 评论 0

leeif 评论了文章 · 2018-09-25

[MySQL进阶之路][No.0001] MySQL的Replication基础

前记

距离上一次在segmentfault上发文章足足过了两年时间,自己也已经从在日本留学进入到了工作岗位。选择留在日本工作的理由其实自己也不是很清楚,只是无论身在哪里,都只想做一个技术人员的理想至少现在并没有改变。虽然目前为止日本的IT行业无论在规模还是技术层面都无法和国内相提并论, 但是自己身边还是有很多大神的,自己在这段时间学到的东西无论如何也想和大家交流分享。还请大家多多指教。

关于这个系列

这个系列主要介绍自己工作上面关于MySQL的运用和研究。这个系列可能会偏向MySQL的底层和架构设计。对于开发方面的SQL语句设计以及数据表的设计等可能只会在介绍索引index的时候稍微提及。最后, 本系列涉及到的MySQL版本将主要集中在5.7和8.0。存储引擎将只介绍Innodb。(主要Myasim等因为自己也没接触过-_-)

为什么要学MySQL

说到为什么要学MySQL,先得说为什么IT公司要用MySQL。MySQL是开源的,你可以在Github上随意的浏览它的源码, 给MySQL开发者送bug report。最主要它是免费的,不管是自己买服务器搭架构,还是用云服务,MySQL都是很好的选择。虽然Oracle, SQL Server等在功能上可能更强大,但是对于中等规模的IT公司来说,MySQL往往已经足够够用了。

那可能就有人会说, 现在谁还用关系型数据库呀。确实现在的数据库种类也是越来越多,NoSQL数据库不断提供着时髦的使用方法,对开发者来说也能更好的节省开发时间。Google的firestore(firebase)在最近也是被日本的开发者们视为掌中宝。但是MySQL在这么多年的企业级使用中,也性能调优方面,数据安全方面也变的不断成熟。虽然现在的NoSQL很方便,但是当涉及到一些敏感或者重要数据的时候,为了数据完整性和安全性,我会选择MySQL。学习MySQL, 对于一个公司的发展或者对于一个技术人员的自我提升来说,其实都是一件很有逼格的事情。(虽然好像没什么说服力)

数据库的复制(Replication)

说了这么对废话,还是快点进入今天的正题。Replicaiton可能是学习数据库架构的最基础的东西了。Replication翻译过来是复制,那就是复制数据库,或者备份数据库呗。那为什么需要复制数据库呢?

想象一下下面一个场景,如果你只有一台数据库服务器,写数据和读数据全都通过这一个数据库来做,当你的流量大了以后,这台服务器的负载将越来越大, 发生故障的机率也越来越大。最后当这台服务器挂掉以后,你的数据库将变的不可用,整个应用死掉,那可能你就要写好多故障报告了。

为了减少上面发生的概率,我们会使用replication,也就是主从架构。一台master(主)服务器底下挂着几台slave(从)服务器。slave数据库通过Replication和master数据库保持数据同步。这时候master数据库可以只用来写数据,读数据的流量就可以分散到slave数据库服务器上了。可之前相比,服务器的负载得到了分散。而且对于这个架构来说,容错性也得到了提高,当一台slave服务器死掉以后,其他或者的slave依然可以接受流量,应用也不会中断。master死掉以后,只要将一台slave升级成master就行了(故障损害虽然不是0,但也能尽可能的减少)。
图片描述

在上图的架构中,有一台slave没有读操作也没有写操作,这个服务器可以被用来定期获取数据库的snapshot。这样做的话就不会因为经常获取snapshot而对生产环境中的服务器造成影响。

MySQL的Replication原理

图片描述

如果在master服务器中设置binlog有效的话,对数据库有更新的操作都会被记录在binlog文件中。(binlog文件将在之后的文章中做详细介绍)
当slave连接到master服务器上时,master会创建一个binlog dump现成。而slave会创建一个IO线程和SQL线程。
具体的复制过程:

  1. master出现数据库更新,在binlog中记录这个更新操作
  2. binlog dump线程binlog中有更新,读取binlog并将它传到连接到的slave。
  3. slave中的IO Thread接受这个binlog,将这个binlog记录在relay log文件中。
  4. slave中的SQL线程从relay log中读取这个更新操作,通过SQL操作将这个更新反应到数据库中
  5. 通过上面的一系列操作,slave和master可以保持一致。

官方文档:https://dev.mysql.com/doc/ref...

实践

方便大家hands-on,可以使用我准备的这个库。(只要装了docker,就可以立马动手了)
https://github.com/leeif/mysq...

在docker中启动MySQL。

//启动container : mysql57_master,mysql57_slave,mysql80_master,mysql80_slave
docker-compose up -d

//进入container里
docker-compose exec mysql57_master(mysql57_slave) bash 

数据库用户root,密码为root。

master

要使用replication,master需要存储binlog。要存储binlog,需要在master中设置指定log-bin(binlog的名字和存储位置)。
※MySQL8.0开始,默认binlog是有效的,无需设置log-bin。

master的配置文件如下:

//mysql57_master

root@76e96aaae65d:/# cat /etc/mysql/conf.d/config-file.cnf
[mysqld]
server-id         = 0001
log-bin           = /var/log/mysql/mysql-bin.log
binlog_format     = statement
binlog_cache_size = 1M
max_binlog_size   = 200M
root@76e96aaae65d:/#

为了让slave识别master,server-id也是必须的。
这时候我们可以查看一下master的状况。

//mysql57_master

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      154 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

mysql>

可以看到当前的binlog文件名是mysql-bin.000003, 并且当前的binlog记录位置是154。
我们尝试在数据库写入一些数据。运行以下脚本。

//mysql57_master

root@76e96aaae65d:/# cat /mysql_etc/mysql_data_generator.sh
#!/bin/bash
mysql -uroot -P 3306 -proot -D mysql -e "create database replication_test;"
mysql -uroot -P 3306 -proot -D mysql -e "create table replication_test.test_table (id int not null auto_increment, name varchar(255), primary key (id));"

data=""
for i in {1..99}; do d="('name_$i'),"; data=$data$d; done
mysql -uroot -P 3306 -proot -D mysql -e "insert into replication_test.test_table (name) values $data('name_100');"
root@76e96aaae65d:/#
//mysql57_master
//写入了100条数据

mysql> select count(*) from replication_test.test_table;
+----------+
| count(*) |
+----------+
|      100 |
+----------+
1 row in set (0.00 sec)

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |     2163 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

mysql>

可以看到binlog的参数发生了改变,说明数据库被更新了,并且更新内容被写入binlog文件里了。

slave

在slave服务器中,我们要让它和master实现同步。首先我们用change master语句让slave知道要从哪个master复制数据。

//mysql57_slave

mysql> change master to MASTER_HOST='10.1.0.100',
    -> MASTER_USER='root',
    -> MASTER_PASSWORD='root',
    -> MASTER_LOG_FILE='mysql-bin.000003',
    -> MASTER_LOG_POS=154;
Query OK, 0 rows affected, 2 warnings (0.12 sec)

mysql>

MASTER_LOG_POS设置成了写入数据之前master的binlog位置。
(这里我们用了root用户,在实际的运用场景中我们一般会在master创建一个只用于replication的用户,给它赋予只能replication的权限。)

启动slave

//mysql57_slave

mysql> start slave;
Query OK, 0 rows affected (0.00 sec)

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.1.0.100
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 2163
               Relay_Log_File: ebd7cc002e88-relay-bin.000002
                Relay_Log_Pos: 2329
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 2163
              Relay_Log_Space: 2543
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 50655a33-bda5-11e8-b007-02420a010064
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
1 row in set (0.00 sec)

mysql>
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
...

从上面的参数重,可以看到IO线程和SQL线程已经在运行,并且没有出现错误,说明replication被成功建立。

查看数据同步情况:

//mysql57_slave

mysql> select count(*) from replication_test.test_table;
+----------+
| count(*) |
+----------+
|      100 |
+----------+
1 row in set (0.01 sec)

mysql>

可以看到master中的数据已经被同步了过来。

查看关于replicaiton的线程

master中运行的process

//mysql57_master

mysql> show processlist\G
*************************** 1. row *************************** //binlog dump线程
     Id: 7
   User: root
   Host: mysql_learning_hard_mysql57_slave_1.mysql_learning_hard_test:
     db: NULL
Command: Binlog Dump
   Time: 693
  State: Master has sent all binlog to slave; waiting for more updates
   Info: NULL
*************************** 2. row ***************************
     Id: 8
   User: root
   Host: localhost
     db: NULL
Command: Query
   Time: 0
  State: starting
   Info: show processlist
2 rows in set (0.00 sec)

mysql>

slave中运行的process

//mysql57_slave

mysql> show processlist\G
*************************** 1. row *************************** //IO线程
     Id: 3
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 790
  State: Waiting for master to send event
   Info: NULL
*************************** 2. row *************************** //SQL线程
     Id: 4
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 92220
  State: Slave has read all relay log; waiting for more updates
   Info: NULL
*************************** 3. row ***************************
     Id: 5
   User: root
   Host: localhost
     db: NULL
Command: Query
   Time: 0
  State: starting
   Info: show processlist
3 rows in set (0.00 sec)

mysql>

结尾

关于MySQL replication的原理以及基本用法就先说到这。下篇准备具体介绍一下show slave status中的参数, 通过这些参数我们可以实时把握当前主从复制的情况。


查看原文

leeif 发布了文章 · 2018-09-23

[MySQL进阶之路][No.0001] MySQL的Replication基础

前记

距离上一次在segmentfault上发文章足足过了两年时间,自己也已经从在日本留学进入到了工作岗位。选择留在日本工作的理由其实自己也不是很清楚,只是无论身在哪里,都只想做一个技术人员的理想至少现在并没有改变。虽然目前为止日本的IT行业无论在规模还是技术层面都无法和国内相提并论, 但是自己身边还是有很多大神的,自己在这段时间学到的东西无论如何也想和大家交流分享。还请大家多多指教。

关于这个系列

这个系列主要介绍自己工作上面关于MySQL的运用和研究。这个系列可能会偏向MySQL的底层和架构设计。对于开发方面的SQL语句设计以及数据表的设计等可能只会在介绍索引index的时候稍微提及。最后, 本系列涉及到的MySQL版本将主要集中在5.7和8.0。存储引擎将只介绍Innodb。(主要Myasim等因为自己也没接触过-_-)

为什么要学MySQL

说到为什么要学MySQL,先得说为什么IT公司要用MySQL。MySQL是开源的,你可以在Github上随意的浏览它的源码, 给MySQL开发者送bug report。最主要它是免费的,不管是自己买服务器搭架构,还是用云服务,MySQL都是很好的选择。虽然Oracle, SQL Server等在功能上可能更强大,但是对于中等规模的IT公司来说,MySQL往往已经足够够用了。

那可能就有人会说, 现在谁还用关系型数据库呀。确实现在的数据库种类也是越来越多,NoSQL数据库不断提供着时髦的使用方法,对开发者来说也能更好的节省开发时间。Google的firestore(firebase)在最近也是被日本的开发者们视为掌中宝。但是MySQL在这么多年的企业级使用中,也性能调优方面,数据安全方面也变的不断成熟。虽然现在的NoSQL很方便,但是当涉及到一些敏感或者重要数据的时候,为了数据完整性和安全性,我会选择MySQL。学习MySQL, 对于一个公司的发展或者对于一个技术人员的自我提升来说,其实都是一件很有逼格的事情。(虽然好像没什么说服力)

数据库的复制(Replication)

说了这么对废话,还是快点进入今天的正题。Replicaiton可能是学习数据库架构的最基础的东西了。Replication翻译过来是复制,那就是复制数据库,或者备份数据库呗。那为什么需要复制数据库呢?

想象一下下面一个场景,如果你只有一台数据库服务器,写数据和读数据全都通过这一个数据库来做,当你的流量大了以后,这台服务器的负载将越来越大, 发生故障的机率也越来越大。最后当这台服务器挂掉以后,你的数据库将变的不可用,整个应用死掉,那可能你就要写好多故障报告了。

为了减少上面发生的概率,我们会使用replication,也就是主从架构。一台master(主)服务器底下挂着几台slave(从)服务器。slave数据库通过Replication和master数据库保持数据同步。这时候master数据库可以只用来写数据,读数据的流量就可以分散到slave数据库服务器上了。可之前相比,服务器的负载得到了分散。而且对于这个架构来说,容错性也得到了提高,当一台slave服务器死掉以后,其他或者的slave依然可以接受流量,应用也不会中断。master死掉以后,只要将一台slave升级成master就行了(故障损害虽然不是0,但也能尽可能的减少)。
图片描述

在上图的架构中,有一台slave没有读操作也没有写操作,这个服务器可以被用来定期获取数据库的snapshot。这样做的话就不会因为经常获取snapshot而对生产环境中的服务器造成影响。

MySQL的Replication原理

图片描述

如果在master服务器中设置binlog有效的话,对数据库有更新的操作都会被记录在binlog文件中。(binlog文件将在之后的文章中做详细介绍)
当slave连接到master服务器上时,master会创建一个binlog dump现成。而slave会创建一个IO线程和SQL线程。
具体的复制过程:

  1. master出现数据库更新,在binlog中记录这个更新操作
  2. binlog dump线程binlog中有更新,读取binlog并将它传到连接到的slave。
  3. slave中的IO Thread接受这个binlog,将这个binlog记录在relay log文件中。
  4. slave中的SQL线程从relay log中读取这个更新操作,通过SQL操作将这个更新反应到数据库中
  5. 通过上面的一系列操作,slave和master可以保持一致。

官方文档:https://dev.mysql.com/doc/ref...

实践

方便大家hands-on,可以使用我准备的这个库。(只要装了docker,就可以立马动手了)
https://github.com/leeif/mysq...

在docker中启动MySQL。

//启动container : mysql57_master,mysql57_slave,mysql80_master,mysql80_slave
docker-compose up -d

//进入container里
docker-compose exec mysql57_master(mysql57_slave) bash 

数据库用户root,密码为root。

master

要使用replication,master需要存储binlog。要存储binlog,需要在master中设置指定log-bin(binlog的名字和存储位置)。
※MySQL8.0开始,默认binlog是有效的,无需设置log-bin。

master的配置文件如下:

//mysql57_master

root@76e96aaae65d:/# cat /etc/mysql/conf.d/config-file.cnf
[mysqld]
server-id         = 0001
log-bin           = /var/log/mysql/mysql-bin.log
binlog_format     = statement
binlog_cache_size = 1M
max_binlog_size   = 200M
root@76e96aaae65d:/#

为了让slave识别master,server-id也是必须的。
这时候我们可以查看一下master的状况。

//mysql57_master

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |      154 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

mysql>

可以看到当前的binlog文件名是mysql-bin.000003, 并且当前的binlog记录位置是154。
我们尝试在数据库写入一些数据。运行以下脚本。

//mysql57_master

root@76e96aaae65d:/# cat /mysql_etc/mysql_data_generator.sh
#!/bin/bash
mysql -uroot -P 3306 -proot -D mysql -e "create database replication_test;"
mysql -uroot -P 3306 -proot -D mysql -e "create table replication_test.test_table (id int not null auto_increment, name varchar(255), primary key (id));"

data=""
for i in {1..99}; do d="('name_$i'),"; data=$data$d; done
mysql -uroot -P 3306 -proot -D mysql -e "insert into replication_test.test_table (name) values $data('name_100');"
root@76e96aaae65d:/#
//mysql57_master
//写入了100条数据

mysql> select count(*) from replication_test.test_table;
+----------+
| count(*) |
+----------+
|      100 |
+----------+
1 row in set (0.00 sec)

mysql> show master status;
+------------------+----------+--------------+------------------+-------------------+
| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+------------------+----------+--------------+------------------+-------------------+
| mysql-bin.000003 |     2163 |              |                  |                   |
+------------------+----------+--------------+------------------+-------------------+
1 row in set (0.00 sec)

mysql>

可以看到binlog的参数发生了改变,说明数据库被更新了,并且更新内容被写入binlog文件里了。

slave

在slave服务器中,我们要让它和master实现同步。首先我们用change master语句让slave知道要从哪个master复制数据。

//mysql57_slave

mysql> change master to MASTER_HOST='10.1.0.100',
    -> MASTER_USER='root',
    -> MASTER_PASSWORD='root',
    -> MASTER_LOG_FILE='mysql-bin.000003',
    -> MASTER_LOG_POS=154;
Query OK, 0 rows affected, 2 warnings (0.12 sec)

mysql>

MASTER_LOG_POS设置成了写入数据之前master的binlog位置。
(这里我们用了root用户,在实际的运用场景中我们一般会在master创建一个只用于replication的用户,给它赋予只能replication的权限。)

启动slave

//mysql57_slave

mysql> start slave;
Query OK, 0 rows affected (0.00 sec)

mysql> show slave status\G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.1.0.100
                  Master_User: root
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000003
          Read_Master_Log_Pos: 2163
               Relay_Log_File: ebd7cc002e88-relay-bin.000002
                Relay_Log_Pos: 2329
        Relay_Master_Log_File: mysql-bin.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 2163
              Relay_Log_Space: 2543
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
                  Master_UUID: 50655a33-bda5-11e8-b007-02420a010064
             Master_Info_File: /var/lib/mysql/master.info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind:
      Last_IO_Error_Timestamp:
     Last_SQL_Error_Timestamp:
               Master_SSL_Crl:
           Master_SSL_Crlpath:
           Retrieved_Gtid_Set:
            Executed_Gtid_Set:
                Auto_Position: 0
         Replicate_Rewrite_DB:
                 Channel_Name:
           Master_TLS_Version:
1 row in set (0.00 sec)

mysql>
...
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
...
Last_IO_Errno: 0
Last_IO_Error:
Last_SQL_Errno: 0
Last_SQL_Error:
...

从上面的参数重,可以看到IO线程和SQL线程已经在运行,并且没有出现错误,说明replication被成功建立。

查看数据同步情况:

//mysql57_slave

mysql> select count(*) from replication_test.test_table;
+----------+
| count(*) |
+----------+
|      100 |
+----------+
1 row in set (0.01 sec)

mysql>

可以看到master中的数据已经被同步了过来。

查看关于replicaiton的线程

master中运行的process

//mysql57_master

mysql> show processlist\G
*************************** 1. row *************************** //binlog dump线程
     Id: 7
   User: root
   Host: mysql_learning_hard_mysql57_slave_1.mysql_learning_hard_test:
     db: NULL
Command: Binlog Dump
   Time: 693
  State: Master has sent all binlog to slave; waiting for more updates
   Info: NULL
*************************** 2. row ***************************
     Id: 8
   User: root
   Host: localhost
     db: NULL
Command: Query
   Time: 0
  State: starting
   Info: show processlist
2 rows in set (0.00 sec)

mysql>

slave中运行的process

//mysql57_slave

mysql> show processlist\G
*************************** 1. row *************************** //IO线程
     Id: 3
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 790
  State: Waiting for master to send event
   Info: NULL
*************************** 2. row *************************** //SQL线程
     Id: 4
   User: system user
   Host:
     db: NULL
Command: Connect
   Time: 92220
  State: Slave has read all relay log; waiting for more updates
   Info: NULL
*************************** 3. row ***************************
     Id: 5
   User: root
   Host: localhost
     db: NULL
Command: Query
   Time: 0
  State: starting
   Info: show processlist
3 rows in set (0.00 sec)

mysql>

结尾

关于MySQL replication的原理以及基本用法就先说到这。下篇准备具体介绍一下show slave status中的参数, 通过这些参数我们可以实时把握当前主从复制的情况。


查看原文

赞 20 收藏 15 评论 4

认证与成就

  • 获得 53 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 3 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-12-02
个人主页被 843 人浏览