如何从 MongoDB 迁移到 MySQL

最近的一个多月时间其实都在做数据库的迁移工作,我目前在开发的项目其实在上古时代是使用 MySQL 作为主要数据库的,后来由于一些业务上的原因从 MySQL 迁移到了 MongoDB,使用了几个月的时间后,由于数据库服务非常不稳定,再加上无人看管,同时 MongoDB 本身就是无 Schema 的数据库,最后导致数据库的脏数据问题非常严重。目前团队的成员没有较为丰富的 Rails 开发经验,所以还是希望使用 ActiveRecord 加上 Migration 的方式对数据进行一些强限制,保证数据库中数据的合法。

未分类

文中会介绍作者在迁移数据库的过程中遇到的一些问题,并为各位读者提供需要停机迁移数据库的可行方案,如果需要不停机迁移数据库还是需要别的方案来解决,在这里提供的方案用于百万数据量的 MongoDB,预计的停机时间在两小时左右,如果数据量在千万级别以上,过长的停机时间可能是无法接受的,应该设计不停机的迁移方案;无论如何,作者希望这篇文章能够给想要做数据库迁移的开发者带来一些思路,少走一些坑。

从关系到文档

虽然这篇文章的重点是从 MongoDB 迁移到 MySQL,但是作者还是想简单提一下从 MySQL 到 MongoDB 的迁移,如果我们仅仅是将 MySQL 中的全部数据导入到 MongoDB 中其实是一间比较简单的事情,其中最重要的原因就是 MySQL 支持的数据类型是 MongoDB 的子集:

未分类

在迁移的过程中可以将 MySQL 中的全部数据以 csv 的格式导出,然后再将所有 csv 格式的数据使用 mongoimport 全部导入到 MongoDB 中:

$ mysqldump -u<username> -p<password> 
    -T <output_directory> 
    --fields-terminated-by ',' 
    --fields-enclosed-by '"' 
    --fields-escaped-by '' 
    --no-create-info <database_name>

$ mongoimport --db <database_name> --collection <collection_name> 
    --type csv 
    --file <data.csv> 
    --headerline

虽然整个过程看起来只需要两个命令非常简单,但是等到你真要去做的时候你会遇到非常多的问题,作者没有过从 MySQL 或者其他关系型数据库迁移到 MongoDB 的经验,但是 Google 上相关的资料特别多,所以这总是一个有无数前人踩过坑的问题,而前人的经验也能够帮助我们节省很多时间。

未分类

使用 csv 的方式导出数据在绝大多数的情况都不会出现问题,但是如果数据库中的某些文档中存储的是富文本,那么虽然在导出数据时不会出现问题,最终导入时可能出现一些比较奇怪的错误。

从文档到关系

相比于从 MySQL 到 MongoDB 的迁移,反向的迁移就麻烦了不止一倍,这主要是因为 MongoDB 中的很多数据类型和集合之间的关系在 MySQL 中都并不存在,比如嵌入式的数据结构、数组和哈希等集合类型、多对多关系的实现,很多的问题都不是仅仅能通过数据上的迁移解决的,我们需要在对数据进行迁移之前先对部分数据结构进行重构,本文中的后半部分会介绍需要处理的数据结构和逻辑。

未分类

当我们准备将数据库彻底迁移到 MySQL 之前,需要做一些准备工作,将最后迁移所需要的工作尽可能地减少,保证停机的时间不会太长,准备工作的目标就是尽量消灭工程中复杂的数据结构。

数据的预处理

在进行迁移之前要做很多准备工作,第一件事情是要把所有嵌入的数据结构改成非嵌入式的数据结构:

未分类

也就是把所有 embeds_many 和 embeds_one 的关系都改成 has_many 和 has_one,同时将 embedded_in 都替换成 belongs_to,同时我们需要将工程中对应的测试都改成这种引用的关系,然而只改变代码中的关系并没有真正改变 MongoDB 中的数据。

def embeds_many_to_has_many(parent, child)
  child_key_name = child.to_s.underscore.pluralize
  parent.collection.find({}).each do |parent_document|
    next unless parent_document[child_key_name]
    parent_document[child_key_name].each do |child_document|
      new_child = child_document.merge "#{parent.to_s.underscore}_id": parent_document['_id']
      child.collection.insert_one new_child
    end
  end
  parent.all.unset(child_key_name.to_sym)
end

embeds_many_to_has_many(Person, Address)

我们可以使用上述的代码将关系为嵌入的模型都转换成引用,拍平所有复杂的数据关系,这段代码的运行时间与嵌入关系中的两个模型的数量有关,需要注意的是,MongoDB 中嵌入模型的数据可能因为某些原因出现相同的 _id 在插入时会发生冲突导致崩溃,你可以对 insert_one 使用 resuce 来保证这段代码的运行不会因为上述原因而停止。

未分类

通过这段代码我们就可以轻松将原有的嵌入关系全部展开变成引用的关系,将嵌入的关系变成引用除了做这两个改变之外,不需要做其他的事情,无论是数据的查询还是模型的创建都不需要改变代码的实现,不过记得为子模型中父模型的外键添加索引,否则会导致父模型在获取自己持有的全部子模型时造成全表扫描:

class Comment
  include Mongoid::Document
  index post_id: 1
  belongs_to :post
end

在处理了 MongoDB 中独有的嵌入式关系之后,我们就需要解决一些复杂的集合类型了,比如数组和哈希,如果我们使用 MySQL5.7 或者 PostgreSQL 的话,其实并不需要对他们进行处理,因为最新版本的 MySQL 和 PostgreSQL 已经提供了对 JSON 的支持,不过作者还是将项目中的数组和哈希都变成了常见的数据结构。

在这个可选的过程中,其实并没有什么标准答案,我们可以根据需要将不同的数据转换成不同的数据结构:

未分类

比如,将数组变成字符串或者一对多关系,将哈希变成当前文档的键值对等等,如何处理这些集合数据其实都要看我们的业务逻辑,在改变这些字段的同时尽量为上层提供一个与原来直接 .tags 或者 .categories 结果相同的 API:

class Post
  ...
  def tag_titles
    tags.map(&:title)
  end

  def split_categories
    categories.split(',')
  end
end

这一步其实也是可选的,上述代码只是为了减少其他地方的修改负担,当然如果你想使用 MySQL5.7 或者 PostgreSQL 数据库对 JSON 的支持也没有什么太大的问题,只是在查询集合字段时有一些不方便。

Mongoid 的『小兄弟』们

在使用 Mongoid 进行开发期间难免会用到一些相关插件,比如 mongoid-enum、mongoid-slug 和 mongoid-history 等,这些插件的实现与 ActiveRecord 中具有相同功能的插件在实现上有很大的不同。

对于有些插件,比如 mongoid-slug 只是在引入插件的模型的文档中插入了 _slugs 字段,我们只需要在进行数据迁移忽略这些添加的字段并将所有的 #slug 方法改成 #id,不需要在预处理的过程中做其它的改变。而枚举的实现在 Mongoid 的插件和 ActiveRecord 中就截然不同了:

未分类

mongoid-enum 使用字符串和 _status 来保存枚举类型的字段,而 ActiveRecord 使用整数和 status 表示枚举类型,两者在底层数据结构的存储上有一些不同,我们会在之后的迁移脚本中解决这个问题。

未分类

如果在项目中使用了很多 Mongoid 的插件,由于其实现不同,我们也只能根据不同的插件的具体实现来决定如何对其进行迁移,如果使用了一些支持特殊功能的插件可能很难在 ActiveRecord 中找到对应的支持,在迁移时可以考虑暂时将部分不重要的功能移除。

主键与 UUID

我们希望从 MongoDB 迁移到 MySQL 的另一个重要原因就是 MongoDB 每一个文档的主键实在是太过冗长,一个 32 字节的 _id 无法给我们提供特别多的信息,只能增加我们的阅读障碍,再加上项目中并没有部署 MongoDB 集群,所以没能享受到用默认的 UUID 生成机制带来的好处。

未分类

我们不仅没有享受到 UUID 带来的有点,它还在迁移 MySQL 的过程中为我们带来了很大的麻烦,一方面是因为 ActiveRecord 的默认主键是整数,不支持 32 字节长度的 UUID,如果我们想要不改变 MongoDB 的 UUID,直接迁移到 MySQL 中使用其实也没有什么问题,只是我们要将默认的整数类型的主键变成字符串类型,同时要使用一个 UUID 生成器来保证所有的主键都是根据时间递增的并且不会冲突。

如果准备使用 UUID 加生成器的方式,其实会省去很多迁移的时间,不过看起来确实不是特别的优雅,如何选择还是要权衡和评估,但是如果我们选择了使用 integer 类型的自增主键时,就需要做很多额外的工作了,首先是为所有的表添加 uuid 字段,同时为所有的外键例如 post_id 创建对应的 post_uuid 字段,通过 uuid 将两者关联起来:

未分类

在数据的迁移过程中,我们会将原有的 _id 映射到 uuid 中,post_id 映射到 post_uuid 上,我们通过保持 uuid 和 post_uuid 之间的关系保证模型之间的关系没有丢失,在迁移数据的过程中 id 和 post_id 是完全不存在任何联系的。

当我们按照 _id 的顺序遍历整个文档,将文档中的数据被插入到表中时,MySQL 会为所有的数据行自动生成的递增的主键 id,而 post_id 在这时都为空。

未分类

在全部的数据都被插入到 MySQL 之后,我们通过 #find_by_uuid 查询的方式将 uuid 和 post_uuid 中的关系迁移到 id 和 post_id 中,并将与 uuid 相关的字段全部删除,这样我们能够保证模型之间的关系不会消失,并且数据行的相对位置与迁移前完全一致。

代码的迁移

Mongoid 在使用时都是通过 include 将相关方法加载到当前模型中的,而 ActiveRecord 是通过继承 ActiveRecord::Base 的方式使用的,完成了对数据的预处理,我们就可以对现有模型层的代码进行修改了。

首先当然是更改模型的『父类』,把所有的 Mongoid::Document 都改成 ActiveRecord::Base,然后创建类对应的 Migration 迁移文件:

# app/models/post.rb
class Post < ActiveRecord::Base
  validate_presence_of :title, :content
end

# db/migrate/20170908075625_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.1]
  def change
    create_table :posts do |t|
      t.string :title, null: false
      t.text :content, null: false
      t.string :uuid, null: false

      t.timestamps null: false
    end

    add_index :posts, :uuid, unique: true
  end
end

注意:要为每一张表添加类型为字符串的 uuid 字段,同时为 uuid 建立唯一索引,以加快通过 uuid 建立不同数据模型之间关系的速度。

除了建立数据库的迁移文件并修改基类,我们还需要修改一些 include 的模块和 Mongoid 中独有的查询,比如使用 gte 或者 lte 的日期查询和使用正则进行模式匹配的查询,这些查询在 ActiveRecord 中的使用方式与 Mongoid 中完全不同,我们需要通过手写 SQL 来解决这些问题。

未分类

除此之外,我们也需要处理一些复杂的模型关系,比如 Mongoid 中的 inverse_of 在 ActiveRecord 中叫做 foreign_key 等等,这些修改其实都并不复杂,只是如果想要将这部分的代码全部处理掉,就需要对业务逻辑进行详细地测试以保证不会有遗留的问题,这也就对我们项目的测试覆盖率有着比较高的要求了,不过我相信绝大多数的 Rails 工程都有着非常好的测试覆盖率,能够保证这一部分代码和逻辑能够顺利迁移,但是如果项目中完全没有测试或者测试覆盖率很低,就只能人肉进行测试或者自求多福了,或者就别做迁移了,多写点测试再考虑这些重构的事情吧。

数据的迁移

为每一个模型创建对应的迁移文件并建表其实一个不得不做的体力活,虽然有一些工作我们没法省略,但是我们可以考虑使用自动化的方式为所有的模型添加 uuid 字段和索引,同时也为类似 post_id 的字段添加相应的 post_uuid 列:

class AddUuidColumns < ActiveRecord::Migration[5.1]
  def change
    Rails.application.eager_load!
    ActiveRecord::Base.descendants.map do |klass|
      # add `uuid` column and create unique index on `uuid`.
      add_column klass.table_name, :uuid, :string, unique: true
      add_index klass.table_name, unique: true

      # add `xxx_uuid` columns, ex: `post_uuid`, `comment_uuid` and etc.
      uuids = klass.attribute_names
        .select { |attr| attr.include? '_id' }
        .map    { |attr| attr.gsub '_id', '_uuid' }
      next unless uuids.present?
      uuids.each do |uuid|
        add_column klass.table_name, uuid, :string
      end
    end
  end
end

在添加 uuid 列并建立好索引之后,我们就可以开始对数据库进行迁移了,如果我们决定在迁移的过程中改变原有数据的主键,那么我们会将迁移分成两个步骤,数据的迁移和关系的重建,前者仅指将 MongoDB 中的所有数据全部迁移到 MySQL 中对应的表中,并将所有的 _id 转换成 uuid、xx_id 转换成 xx_uuid,而后者就是前面提到的:通过 uuid 和 xx_uuid 的关联重新建立模型之间的关系并在最后删除所有的 uuid 字段。

我们可以使用如下的代码对数据进行迁移,这段代码从 MongoDB 中遍历某个集合 Collection 中的全部数据,然后将文档作为参数传入 block,然后再分别通过 DatabaseTransformer#delete_obsolete_columns 和 DatabaseTransformer#update_rename_columns 方法删除部分已有的列、更新一些数据列最后将所有的 id 列都变成 uuid:

module DatabaseTransformer
  def import(collection_name, *obsolete_columns, **rename_columns)
    collection = Mongoid::Clients.default.collections.select do |c|
      c.namespace == "#{database}.#{collection_name.to_s.pluralize}"
    end.first

    unless collection.present?
      STDOUT.puts "#{collection_name.to_s.yellow}: skipped"
      STDOUT.puts
      return
    end

    constant = collection_name.to_s.singularize.camelcase.constantize
    reset_callbacks constant

    DatabaseTransformer.profiling do
      collection_count = collection.find.count
      collection.find.each_with_index do |document, index|
        document = yield document if block_given?
        delete_obsolete_columns document, obsolete_columns
        update_rename_columns document, rename_columns
        update_id_columns document

        insert_record constant, document
        STDOUT.puts "#{index}/#{collection_count}n" if (index % 1000).zero?
      end
    end
  end
end

当完成了对文档的各种操作之后,该方法会直接调用 DatabaseTransformer#insert_record 将数据插入 MySQL 对应的表中;我们可以直接使用如下的代码将某个 Collection 中的全部文档迁移到 MySQL 中:

transformer = DatabaseTransformer.new 'draven_production'
transformer.import :post, :_slugs, name: :title, _status: :status

上述代码会在迁移时将集合每一个文档的 _slugs 字段全部忽略,同时将 name 重命名成 title、_status 重命名成 status,虽然作为枚举类型的字段 mongoid-enum 和 ActiveRecord 的枚举类型完全不同,但是在这里可以直接插入也没有什么问题,ActiveRecord 的模型在创建时会自己处理字符串和整数之间的转换:

def insert_record(constant, params)
  model = constant.new params
  model.save! validate: false
rescue Exception => exception
  STDERR.puts "Import Error: #{exception}"
  raise exception
end

为了加快数据的插入速度,同时避免所有由于插入操作带来的副作用,我们会在数据迁移期间重置所有的回调:

def reset_callbacks(constant)
  %i(create save update).each do |callback|
    constant.reset_callbacks callback
  end
end

这段代码的作用仅在这个脚本运行的过程中才会生效,不会对工程中的其他地方造成任何的影响;同时,该脚本会在每 1000 个模型插入成功后向标准输出打印当前进度,帮助我们快速发现问题和预估迁移的时间。

你可以在 https://gist.github.com/Draveness/10476fe67a10128a37ba27a4c6967d07 找到完整的数据迁移代码。

将所有的数据全部插入到 MySQL 的表之后,模型之间还没有任何显式的关系,我们还需要将通过 uuid 连接的模型转换成使用 id 的方式,对象之间的关系才能通过点语法直接访问,关系的建立其实非常简单,我们获得当前类所有结尾为 _uuid 的属性,然后遍历所有的数据行,根据 uuid 的值和 post_uuid 属性中的 “post” 部分获取到表名,最终得到对应的关联模型,在这里我们也处理了类似多态的特殊情况:

module RelationBuilder
  def build_relations(class_name, polymorphic_associations = [], rename_associations = {})
    uuids = class_name.attribute_names.select { |name| name.end_with? '_uuid' }

    unless uuids.present?
      STDOUT.puts "#{class_name.to_s.yellow}: skipped"
      STDOUT.puts
      return
    end

    reset_callbacks class_name

    RelationBuilder.profiling do
      models_count = class_name.count
      class_name.unscoped.all.each_with_index do |model, index|
        update_params = uuids.map do |uuid|
          original_association_name = uuid[0...-5]

          association_model = association_model(
            original_association_name,
            model[uuid],
            polymorphic_associations,
            rename_associations
          )

          [original_association_name.to_s, association_model]
        end.compact

        begin
          Hash[update_params].each do |key, value|
            model.send "#{key}=", value
          end
          model.save! validate: false
        rescue Exception => e
          STDERR.puts e
          raise e
        end

        STDOUT.puts "#{index}/#{models_count}n" if (counter % 1000).zero?
      end
    end
  end
end

在查找到对应的数据行之后就非常简单了,我们调用对应的 post= 等方法更新外键最后直接将外键的值保存到数据库中,与数据的迁移过程一样,我们在这段代码的执行过程中也会打印出当前的进度。

在初始化 RelationBuilder 时,如果我们传入了 constants,那么在调用 RelationBuilder#build! 时就会重建其中的全部关系,但是如果没有传入就会默认加载 ActiveRecord 中所有的子类,并去掉其中包含 :: 的模型,也就是 ActiveRecord 中使用 has_and_belongs_to_many 创建的中间类,我们会在下一节中介绍如何单独处理多对多关系:

def initialize(constants = [])
  if constants.present?
    @constants = constants
  else
    Rails.application.eager_load!
    @constants = ActiveRecord::Base.descendants
        .reject { |constant| constant.to_s.include?('::') }
  end
end

跟关系重建相关的代码可以在 https://gist.github.com/Draveness/c0798fb1272f483a176fa67741a3f1ee 找到完整的用于关系迁移的代码。

builder = RelationBuilder.new([Post, Comment])
builder.build!

通过这数据迁移和关系重建两个步骤就已经可以解决绝大部分的数据迁移问题了,但是由于 MongoDB 和 ActiveRecord 中对于多对多关系的处理比较特殊,所以我们需要单独进行解决,如果所有的迁移问题到这里都已经解决了,那么我们就可以使用下面的迁移文件将数据库中与 uuid 有关的全部列都删除了:

class RemoveAllUuidColumns < ActiveRecord::Migration[5.1]
  def change
    Rails.application.eager_load!
    ActiveRecord::Base.descendants.map do |klass|
      attrs = klass.attribute_names.select { |n| n.include? 'uuid' }
      next unless attrs.present?
      remove_columns klass.table_name, *attrs
    end
  end
end

到这里位置整个迁移的过程就基本完成了,接下来就是跟整个迁移过程中有关的其他事项,例如:对多对关系、测试的重要性等话题。

多对多关系的处理

多对多关系在数据的迁移过程中其实稍微有一些复杂,在 Mongoid 中使用 has_and_belongs_to_many 会在相关的文档下添加一个 tag_ids 或者 post_ids 数组:

# The post document.
{
  "_id" : ObjectId("4d3ed089fb60ab534684b7e9"),
  "tag_ids" : [
    ObjectId("4d3ed089fb60ab534684b7f2"), 
    ObjectId("4d3ed089fb60ab53468831f1")
  ],
  "title": "xxx",
  "content": "xxx"
}

而 ActiveRecord 中会建立一张单独的表,表的名称是两张表名按照字母表顺序的拼接,如果是 Post 和 Tag,对应的多对多表就是 posts_tags,除了创建多对多表,has_and_belongs_to_many 还会创建两个 ActiveRecord::Base 的子类 Tag::HABTM_Posts 和 Post::HABTM_Tags,我们可以使用下面的代码简单实验一下:

require 'active_record'

class Tag < ActiveRecord::Base; end
class Post < ActiveRecord::Base
  has_and_belongs_to_many :tags
end
class Tag < ActiveRecord::Base
  has_and_belongs_to_many :posts
end
puts ActiveRecord::Base.descendants
# => [Tag, Post, Post::HABTM_Tags, Tag::HABTM_Posts]

上述代码打印出了两个 has_and_belongs_to_many 生成的类 Tag::HABTM_Posts 和 Post::HABTM_Tags,它们有着完全相同的表 posts_tags,处理多对多关系时,我们只需要在使用 DatabaseTransformer 导入表中的所有的数据之后,再通过遍历 posts_tags 表中的数据更新多对多的关系表就可以了:

class PostsTag < ActiveRecord::Base; end

# migrate data from mongodb to mysql.
transformer = DatabaseTransformer.new 'draven_production'
transformer.import :posts_tags

# establish association between posts and tags.
PostsTag.unscoped.all.each do |model|
  post = Post.find_by_uuid model.post_uuid
  tag = Tag.find_by_uuid model.tag_uuid
  next unless post.present? && tag.present?
  model.update_columns post_id: post.id, tag_id: tag.id
end

所有使用 has_and_belongs_to_many 的多对多关系都需要通过上述代码进行迁移,这一步需要在删除数据库中的所有 uuid 字段之前完成。

测试的重要性

在真正对线上的服务进行停机迁移之前,我们其实需要对数据库已有的数据进行部分和全量测试,在部分测试阶段,我们可以在本地准备一个数据量为生产环境数据量 1/10 或者 1/100 的 MongoDB 数据库,通过在本地模拟 MongoDB 和 MySQL 的环境进行预迁移,确保我们能够尽快地发现迁移脚本中的错误。

未分类

准备测试数据库的办法是通过关系删除一些主要模型的数据行,在删除时可以通过 MongoDB 中的 dependent: :destroy 删除相关的模型,这样可以尽可能的保证数据的一致性和完整性,但是在对线上数据库进行迁移之前,我们依然需要对 MongoDB 中的全部数据进行全量的迁移测试,这样可以发现一些更加隐蔽的问题,保证真正上线时可以出现更少的状况。

数据库的迁移其实也属于重构,在进行 MongoDB 的数据库迁移之前一定要保证项目有着完善的测试体系和测试用例,这样才能让我们在项目重构之后,确定不会出现我们难以预料的问题,整个项目才是可控的,如果工程中没有足够的测试甚至没有测试,那么就不要再说重构这件事情了 – 单元测试是重构的基础。

总结

如何从 MongoDB 迁移到 MySQL 其实是一个工程问题,我们需要在整个过程中不断寻找可能出错的问题,将一个比较复杂的任务进行拆分,在真正做迁移之前尽可能地减少迁移对服务可用性以及稳定性带来的影响。

未分类

除此之外,MongoDB 和 MySQL 之间的选择也不一定是非此即彼,我们将项目中的大部分数据都迁移到了 MySQL 中,但是将一部分用于计算和分析的数据留在了 MongoDB,这样就可以保证 MongoDB 宕机之后仍然不会影响项目的主要任务,同时,MySQL 的备份和恢复速度也会因为数据库变小而非常迅速。

最后一点,测试真的很重要,如果没有测试,没有人能够做到在修改大量的业务代码的过程中不丢失任何的业务逻辑,甚至如果没有测试,很多业务逻辑可能在开发的那一天就已经丢失了。

如果对文章的内容有疑问或者有 MongoDB 迁移相关的问题,可以在评论中留言,评论系统使用 Disqus 需要梯子。

node+mongodb建站攻略

如何利用node+mongodb来快速搭建一个电影网站?

一、后端部分

整个网站的后端是由node.js来驱动的,所以在后端需要安装node.js,以及在这个基础之上的框架express,它能够帮助我快速的搭建web应用,然后数据库选用的mongodb,以及对mongodb快速建模的工具mongoose。后端的模板引擎用的是jade关于时间个日期的格式化选用的是Moment.js底下的这四个模块都是通过npm安装的,npm是node自带的。

未分类

二、前端部分

前端部分会选用jQuery的类库以及boostrap的样式的框架,它们都是网站前端的静态资源,网站前端的静态资源都存在一个版本的依赖和版本的管理,所以我们需要通过Bower来安装它们,Bower本身也是一个npm模块,所以它也需要现有npm的安装方。

未分类

三、本地的开发环境

本地的开发环境后面的网站会用到less的编译,样式的合并,语法的检查,包括前后端单元测试的实现,以及服务的自动重启,这几个任务都是通过grunt来集成。

未分类

四、实战步骤

选择完这些框架以后,就可以来开始实战,项目开发分为九个步骤

未分类

1、需求分析:看一下开发一共有多少个页面,页面里都有什么样的内容和需求以及什么样的交互,这样做到心里有数。

2、项目依赖初始化:然后对项目所依赖的模块进行一个安装和 初始目录的创建。

3、入口文件编码:在后端开始创建一个入口文件并且进行编码。

4、创建试图:编码以后就可以来创建几个重要页面的视图了,也就是模板。

5、测试前端流程 :来跑通前后端的流程,也就是说,从浏览器发起一个请求到后端,后端接收到之后,返回一段数据。

6、样式开发,伪造模板数据  :跑通前后端之后,就可以对页面进行一个样式的开发和一些HTML的dom结构的填充,同时要伪造一些模板数据。

7、设计数据库模型:这时候页面都有了,我们开始基于页面里面的内容同时来设计数据库的模型。

8、开发后端逻辑:然后来开发后端的逻辑,到这一步为止,前后端的逻辑都已经实现掉了。

9、配置依赖文件,网站开发结束:对前端静态资源版本和后端模块版本进行一个配置文件的生成,然后整个网站就开发结束了。

五、详细开发

然后来看一下网站的页面 ,左边是首页,有一个电影列表,把存在数据库里面的电影全部都取出来展现在这里,每一个海报都有电影名称和播放按钮,点击海报或者播放按钮就会跳转到右边这个页面,也就是详情页,详情页就是这部电影所针对的更加详细的介绍字段之类的东西。

未分类

后台录入页:其实这里就是一个表单,在后端能够填写一些和电影有关的数据,点击最底下的录入按钮的时候,会将这张表上的数据提交到后台,后台就会存到数据库,也就是mongodb里面,右边是列表页,当存入一定数据的电影的时候,我们可以进行批量的管理,比如更新或者删除。

未分类

Docker配置NodeJS+MongoDB

未分类

需求

在Docker中运行一个Nodejs项目,连接Mongo数据库进行数据操作,并启动RockMongo容器。

思路

  • 使用容器连接(link)方法,Mongo容器与WebApp容器连接
  • 数据库连接语句要通过容器别名构建!!!
  • 宿主机只需监听Web端口即可
  • Rockmongo用于图形化界面管理数据库

前期准备

服务器上已有nodejs环境并可运行,根目录在/usr/local/node
项目中的数据库连接语句改成数据库容器别名,实例化Mongo容器时–name casebasedb,mongoose连接语句改为

var store = new SessionStore({url: 'mongodb://containerdb/session',interval: 1440});
mongoose.connect('mongodb://containerdb:27017/casebasedb', {useMongoClient: true,});

拉取镜像

$ sudo docker pull node
$ sudo docker pull mongo
$ sudo docker pull pataquets/rockmongo

构建Web镜像

编写Dockerfile

$ vim /***/Dockerfile

Dockerfile内容

FROM node:latest # 基于node镜像
COPY . /***/casebase # 项目源文件目录
COPY . /usr/local/node # 直接拖进去的node环境
WORKDIR /***/casebase # nodejs运行目录
RUN npm install # 重新安装下依赖包
EXPOSE 8001 # 暴露端口8001
CMD ["node", "app.js"] # 执行命令

根据Dockerfile新建镜像,命令结尾的 . 是指示Dockerfile文件在当前目录下

$ sudo docker build -t hugh/casebase .

实例化容器

  • -p映射到当前8001端口;
  • –link语句连接数据库 [数据库容器名]:[容器别名]
  • 当连接成功后在web容器中可以直接ping到[容器别名containerdb]作为主机标识,所以一定要将数据库连接语句中的主机改成[容器别名]!!!
$ sudo docker run -d --name casebasedb mongo
$ sudo docker run --name casebaseapp -d -p 8001:8001 --link casebasedb:containerdb hugh/casebase 
$ sudo docker run --name rockapp -d -p 8003:80 --link casebasedb:containerdb pataquets/rockmongo

验证容器

查看容器情况

$ sudo docker ps -a
CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                    NAMES
4e2ab928a683        dh/casebase:17.09.16.1   "node app.js"            14 minutes ago      Up 14 minutes       0.0.0.0:8001->8001/tcp   casebaseapp
66e6ba5f5873        mongo                    "docker-entrypoint..."   17 minutes ago      Up 17 minutes       27017/tcp                casebasedb

查看端口监听情况

$ sudo netstat -lntp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp6       0      0 :::8001                 :::*                    LISTEN      32211/docker-proxy

访问宿主机ip:8001,此时已可以访问web项目了。

mongodb 常用操作

连接mongodb

mongo

默认连接到本地端口27017

mongo –host 123.57.244.111 –port 27017 

连接远程数据库

插入数据

插入一条数据:

db.test.insert({title: “1111111111111111111”})

循环插入数据:

for (var i = 0; i < 100; i++) { 
db.test.insert({title: i * 1000}) 
}

插入数组:

db.test.insert([{title: “aaaaaaaaaaaaa”}, {title: “bbbbbbbbbbbb”}])

聚合

插入一个0到100的随机年龄

for (var i = 0; i < 100; i++) { 
db.test.insert({age: Math.round(Math.random() * 100)}); 
}

统计同一个年龄出现的次数

db.test.aggregate([{group: {_id: "age", count: {$sum: 1}}}])

查询

查询所有数据

db.test.find()

查询大于90000的

db.test.find({title: {$gt: 90000}})

查询大于90000小于93000

db.test.find({title: {gt:90000,lt: 93000}})

查询一条数据

db.test.findOne()

查询存在字段name的

db.test.find({name: {$exists: true}})

查询操作符

  • eq:等于gt:大于
  • gte:大于等于in:匹配数组中指定的任何值
  • lt:大于lte:小于等于
  • ne:匹配不等于指定值的所有值nin:不匹配数组中指定的值
  • size:数量为n的数组exists:查询存在某个字段
  • $type:检索集合中匹配的数据类型

删除记录

删除title大于等于2000的

db.test.remove({title: {$gte: 2000}})

删除所有记录

db.test.remove({})

limit方法

该参数指定从MongoDB中读取的记录条数

读取两条记录

db.test.find().limit(2)

skip方法

跳过指定数量的数据

读取第三条数据

db.test.find().skip(2)

sort方法

对数据进行排序,可以通过参数指定排序的字段,并使用1和-1来指定排序的方式,其中1为升序排列,而-1是用于降序排列

根据title升序排序

db.test.find().sort({title: 1})

distinct方法

返回指定字段的非重复值组成的数组

db.test.distinct(“title”)

查询创建的索引

db.test.getIndexes() 

结果:

[
        {
                "v" : 2,
                "key" : {
                        "_id" : 1
                },
                "name" : "_id_",
                "ns" : "nodeblog.test"
        },
        {
                "v" : 2,
                "key" : {
                        "title" : 1,
                        "count" : 1
                },
                "name" : "title_1_count_1",
                "ns" : "nodeblog.test"
        }
]

删除索引

通过上面查到的name,如:title_1_count_1

db.test.dropIndex(“title_1_count_1”)

创建索引

db.test.createIndex({title: 1}) 

1:升序, -1:降序

创建联合索引:db.test.createIndex({title: 1, count: 1})

查看命令执行时间

在命令后面增加:explain(“executionStats”)
如:db.test.find({title: “hello99999”}).explain(“executionStats”)
结果中的executionTimeMillis就是花费的时间

删除文档

如:删除所有count大于10的文档
db.test.remove({count: {$gt: 10}})

主从复制

下面两个路径必须先创建

1>开启主服务:mongod –master –dbpath e://MongoDB/data/master –port 8888
2>开启从服务:mongod –slave –dbpath e://MongoDB//data//slave –sources 127.0.0.1:8888 –port 8881

可以看到每秒没主服务那边同步数据的日志
[replslave] syncing from host:127.0.0.1:8888

在master中插入一些数据,直接读取slave服务中的数据是不行的。所以关掉master,slave服务,然后按正常方式启动slave
服务,如:mongod –dbpath E://MongoDB//data//slave –port 8881

连接到slave,如:mongo –port 8881,查看之前从master同步过来的数据,如:db.test.find()

监控状态,每秒刷新一次

mongostat –port 27017

查看数据库

show dbs

使用数据库

use users 

只有执行了use才能查找表里的内容

查看当前数据库中的集合(表)

show collections

导出数据库

mongodump -d nodeblog -o /root/db/bk20170908 

-d:导出的数据库名
-o:导出的目录

恢复数据库

mongorestore -d nodeblog –drop e://MongoDB//data//nodeblog 

–drop:删除原有数据库内容

使用 Docker 和 Kubernetes 将 MongoDB 作

介绍

想在笔记本电脑上尝试 MongoDB?只需执行一个命令,你就会有一个轻量级的、独立的沙箱。完成后可以删除你所做的所有痕迹。

想在多个环境中使用相同的程序栈application stack副本?构建你自己的容器镜像,让你的开发、测试、运维和支持团队使用相同的环境克隆。

容器正在彻底改变整个软件生命周期:从最早的技术性实验和概念证明,贯穿了开发、测试、部署和支持。

编排工具用来管理如何创建、升级多个容器,并使之高可用。编排还控制容器如何连接,以从多个微服务容器构建复杂的应用程序。

丰富的功能、简单的工具和强大的 API 使容器和编排功能成为 DevOps 团队的首选,将其集成到连续集成(CI) 和连续交付 (CD) 的工作流程中。

这篇文章探讨了在容器中运行和编排 MongoDB 时遇到的额外挑战,并说明了如何克服这些挑战。

MongoDB 的注意事项

使用容器和编排运行 MongoDB 有一些额外的注意事项:

  • MongoDB 数据库节点是有状态的。如果容器发生故障并被重新编排,数据则会丢失(能够从副本集的其他节点恢复,但这需要时间),这是不合需要的。为了解决这个问题,可以使用诸如 Kubernetes 中的 数据卷volume 抽象等功能来将容器中临时的 MongoDB 数据目录映射到持久位置,以便数据在容器故障和重新编排过程中存留。

  • 一个副本集中的 MongoDB 数据库节点必须能够相互通信 – 包括重新编排后。副本集中的所有节点必须知道其所有对等节点的地址,但是当重新编排容器时,可能会使用不同的 IP 地址重新启动。例如,Kubernetes Pod 中的所有容器共享一个 IP 地址,当重新编排 pod 时,IP 地址会发生变化。使用 Kubernetes,可以通过将 Kubernetes 服务与每个 MongoDB 节点相关联来处理,该节点使用 Kubernetes DNS 服务提供“主机名”,以保持服务在重新编排中保持不变。

  • 一旦每个单独的 MongoDB 节点运行起来(每个都在自己的容器中),则必须初始化副本集,并添加每个节点到其中。这可能需要在编排工具之外提供一些额外的处理。具体来说,必须使用目标副本集中的一个 MongoDB 节点来执行 rs.initiate 和 rs.add 命令。

  • 如果编排框架提供了容器的自动化重新编排(如 Kubernetes),那么这将增加 MongoDB 的弹性,因为这可以自动重新创建失败的副本集成员,从而在没有人为干预的情况下恢复完全的冗余级别。

  • 应该注意的是,虽然编排框架可能监控容器的状态,但是不太可能监视容器内运行的应用程序或备份其数据。这意味着使用 MongoDB Enterprise Advanced [1] 和 MongoDB Professional [2] 中包含的 MongoDB Cloud Manager [3] 等强大的监控和备份解决方案非常重要。可以考虑创建自己的镜像,其中包含你首选的 MongoDB 版本和 MongoDB Automation Agent [4]。

使用 Docker 和 Kubernetes 实现 MongoDB 副本集

如上节所述,分布式数据库(如 MongoDB)在使用编排框架(如 Kubernetes)进行部署时,需要稍加注意。本节将介绍详细介绍如何实现。

我们首先在单个 Kubernetes 集群中创建整个 MongoDB 副本集(通常在一个数据中心内,这显然不能提供地理冗余)。实际上,很少有必要改变成跨多个集群运行,这些步骤将在后面描述。

副本集的每个成员将作为自己的 pod 运行,并提供一个公开 IP 地址和端口的服务。这个“固定”的 IP 地址非常重要,因为外部应用程序和其他副本集成员都可以依赖于它在重新编排 pod 的情况下保持不变。

下图说明了其中一个 pod 以及相关的复制控制器和服务。

未分类

逐步介绍该配置中描述的资源:

  • 从核心开始,有一个名为 mongo-node1 的容器。 mongo-node1 包含一个名为 mongo 的镜像,这是一个在 Docker Hub [5] 上托管的一个公开可用的 MongoDB 容器镜像。容器在集群中暴露端口 27107。

  • Kubernetes 的数据卷功能用于将连接器中的 /data/db 目录映射到名为 mongo-persistent-storage1 的永久存储上,这又被映射到在 Google Cloud 中创建的名为 mongodb-disk1 的磁盘中。这是 MongoDB 存储其数据的地方,这样它可以在容器重新编排后保留。

  • 容器保存在一个 pod 中,该 pod 中有标签命名为 mongo-node,并提供一个名为 rod 的(任意)示例。

  • 配置 mongo-node1 复制控制器以确保 mongo-node1 pod 的单个实例始终运行。

  • 名为 mongo-svc-a 的 负载均衡 服务给外部开放了一个 IP 地址以及 27017 端口,它被映射到容器相同的端口号上。该服务使用选择器来匹配 pod 标签来确定正确的 pod。外部 IP 地址和端口将用于应用程序以及副本集成员之间的通信。每个容器也有本地 IP 地址,但是当容器移动或重新启动时,这些 IP 地址会变化,因此不会用于副本集。

下一个图显示了副本集的第二个成员的配置。

未分类

90% 的配置是一样的,只有这些变化:

  • 磁盘和卷名必须是唯一的,因此使用的是 mongodb-disk2 和 mongo-persistent-storage2

  • Pod 被分配了一个 instance: jane 和 name: mongo-node2 的标签,以便新的服务可以使用选择器与图 1 所示的 rod Pod 相区分。

  • 复制控制器命名为 mongo-rc2

  • 该服务名为 mongo-svc-b,并获得了一个唯一的外部 IP 地址(在这种情况下,Kubernetes 分配了 104.1.4.5)

第三个副本成员的配置遵循相同的模式,下图展示了完整的副本集:

未分类

请注意,即使在三个或更多节点的 Kubernetes 群集上运行图 3 所示的配置,Kubernetes 可能(并且经常会)在同一主机上编排两个或多个 MongoDB 副本集成员。这是因为 Kubernetes 将三个 pod 视为属于三个独立的服务。

为了在区域内增加冗余,可以创建一个附加的 headless 服务。新服务不向外界提供任何功能(甚至不会有 IP 地址),但是它可以让 Kubernetes 通知三个 MongoDB pod 形成一个服务,所以 Kubernetes 会尝试在不同的节点上编排它们。

未分类

配置和启动 MongoDB 副本集所需的实际配置文件和命令可以在白皮书《启用微服务:阐述容器和编排[6]》中找到。特别的是,需要一些本文中描述的特殊步骤来将三个 MongoDB 实例组合成具备功能的、健壮的副本集。

多个可用区 MongoDB 副本集

上面创建的副本集存在风险,因为所有内容都在相同的 GCE 集群中运行,因此都在相同的可用区availability zone中。如果有一个重大事件使可用区离线,那么 MongoDB 副本集将不可用。如果需要地理冗余,则三个 pod 应该在三个不同的可用区或地区中运行。

令人惊奇的是,为了创建在三个区域之间分割的类似的副本集(需要三个集群),几乎不需要改变。每个集群都需要自己的 Kubernetes YAML 文件,该文件仅为该副本集中的一个成员定义了 pod、复制控制器和服务。那么为每个区域创建一个集群,永久存储和 MongoDB 节点是一件很简单的事情。

未分类

下一步

要了解有关容器和编排的更多信息 – 所涉及的技术和所提供的业务优势 – 请阅读白皮书《启用微服务:阐述容器和编排[7]》。该文件提供了获取本文中描述的副本集,并在 Google Container Engine 中的 Docker 和 Kubernetes 上运行的完整的说明。

mongodb 3.4 集群搭建升级版 五台集群

最新版mongodb推荐使用yaml语法来做配置,另外一些旧的配置在最新版本中已经不在生效,所以我们在生产实际搭建mongodb集群的时候做了一些改进。

和前一个版本相比,改动点有:

  • 配置文件采用yaml方式来配置
  • 生产中取消了仲裁者的角色,因为仲裁者也不会存储数据,只是起到选举的作用,线上为了保证数据安全,每份数据都会配置两个副本集,也就是每份数据存储了三份。
  • 优化配置,采用五台集群
  • 使用非root账户搭建mongodb集群。

一、环境准备

  • 系统系统 centos6.9
  • 五台服务器:192.168.0.31/32/33/34/35
  • 安装包: mongodb-linux-x86_64-3.4.6.tgz

服务器规划

未分类

端口分配:

mongos:20000
config:21000
shard1:27001
shard2:27002
shard3:27003
shard4:27004
shard5:27005

权限分配:

登录root账户,将安装目录和数据目录权限分配给日常操作(youknow)账户

chown -R youknow:youknow /usr/local/
chown -R youknow:youknow /data

二、mongodb安装

1、下载

下载 mongodb 3.4.6 安装包

curl -O https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-3.4.6.tgz
#解压
tar -xzvf mongodb-linux-x86_64-3.4.6.tgz -C /usr/local/
#改名
mv mongodb-linux-x86_64-3.4.6 mongodb

2、创建相关目录

根据服务器的规范,分别在对应的服务器上建立conf、mongos、config、shard1、shard2、shard3、shard4、shard5等目录,因为mongos不存储数据,只需要建立日志文件目录即可。

mkdir -p /usr/local/mongodb/conf
mkdir -p /data/mongos/log
mkdir -p /data/config/data
mkdir -p /data/config/log
mkdir -p /data/shard1/data
mkdir -p /data/shard1/log
mkdir -p /data/shard2/data
mkdir -p /data/shard2/log
mkdir -p /data/shard3/data
mkdir -p /data/shard3/log
mkdir -p /data/shard4/data
mkdir -p /data/shard4/log
mkdir -p /data/shard5/data
mkdir -p /data/shard5/log

3、环境变量

为了后续方便操作,配置mongodb的环境变量,需要切到root用户下面

vim /etc/profile
# 内容
export MONGODB_HOME=/usr/local/mongodb
export PATH=$MONGODB_HOME/bin:$PATH
# 使立即生效,在安装用户下(youknow)执行
source /etc/profile

查看mongodb版本信息mongod -v 输出版本信息表明配置环境变量成功

三、集群配置

1、config server配置服务器

在服务器33、34、35上配置以下内容:

添加配置文件:

vi /usr/local/mongodb/conf/config.conf

## content
systemLog:
  destination: file
  logAppend: true
  path: /data/config/log/config.log

# Where and how to store data.
storage:
  dbPath: /data/config/data
  journal:
    enabled: true
# how the process runs
processManagement:
  fork: true
  pidFilePath: /data/config/log/configsrv.pid

# network interfaces
net:
  port: 21000
  bindIp: 192.168.0.33

#operationProfiling:
replication:
    replSetName: config        

sharding:
    clusterRole: configsvr

启动三台服务器的config server

numactl --interleave=all mongod --config /usr/local/mongodb/conf/config.conf

登录任意一台配置服务器,初始化配置副本集

#连接
mongo 192.168.0.33:21000
#config变量
config = {
...    _id : "config",
...     members : [
...         {_id : 0, host : "192.168.0.33:21000" },
...         {_id : 1, host : "192.168.0.34:21000" },
...         {_id : 2, host : "192.168.0.35:21000" }
...     ]
... }

#初始化副本集
rs.initiate(config)

#查看分区状态
rs.status();

其中,”_id” : “configs”应与配置文件中配置的 replicaction.replSetName 一致,”members” 中的 “host” 为三个节点的ip和port

这样配置服务器就配置好了

2、配置分片、副本集

配置第一个分片副本集

在服务器 31、32、33上面做以下配置

配置文件

vi /usr/local/mongodb/conf/shard1.conf

#配置文件内容
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /data/shard1/log/shard1.log

# Where and how to store data.
storage:
  dbPath: /data/shard1/data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 20

# how the process runs
processManagement:
  fork: true 
  pidFilePath: /data/shard1/log/shard1.pid

# network interfaces
net:
  port: 27001
  bindIp: 192.168.0.33

#operationProfiling:
replication:
    replSetName: shard1
sharding:
    clusterRole: shardsvr

启动三台服务器的shard1 server

numactl --interleave=all mongod  --config  /usr/local/mongodb/conf/shard1.conf

登陆任意一台服务器,初始化副本集

mongo 192.168.0.31:27001
#使用admin数据库
use admin
#定义副本集配置
config = {
...    _id : "shard1",
...     members : [
...         {_id : 0, host : "192.168.0.31:27001" },
...         {_id : 1, host : "192.168.0.32:27001" },
...         {_id : 2, host : "192.168.0.33:27001" }
...     ]
... }


#初始化副本集配置
rs.initiate(config);


#查看分区状态
rs.status();

配置第二个分片副本集

在服务器32、33、34上面做以下配置

配置文件

vi /usr/local/mongodb/conf/shard2.conf

#配置文件内容
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /data/shard2/log/shard2.log

# Where and how to store data.
storage:
  dbPath: /data/shard2/data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 20

# how the process runs
processManagement:
  fork: true 
  pidFilePath: /data/shard2/log/shard2.pid

# network interfaces
net:
  port: 27002
  bindIp: 192.168.0.33


#operationProfiling:
replication:
    replSetName: shard2
sharding:
    clusterRole: shardsvr

启动三台服务器的shard2 server

numactl --interleave=all mongod  --config /usr/local/mongodb/conf/shard2.conf

登陆任意一台服务器,初始化副本集

mongo 192.168.0.32:27002
#使用admin数据库
use admin
#定义副本集配置
config = {
...    _id : "shard2",
...     members : [
...         {_id : 0, host : "192.168.0.32:27002" },
...         {_id : 1, host : "192.168.0.33:27002" },
...         {_id : 2, host : "192.168.0.34:27002" }
...     ]
... }


#初始化副本集配置
rs.initiate(config);

#查看分区状态
rs.status();

配置第三个分片副本集

在服务器33、34、35上面做以下配置

配置文件

vi /usr/local/mongodb/conf/shard3.conf

#配置文件内容
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /data/shard3/log/shard3.log

# Where and how to store data.
storage:
  dbPath: /data/shard3/data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 20
# how the process runs
processManagement:
  fork: true 
  pidFilePath: /data/shard3/log/shard3.pid

# network interfaces
net:
  port: 27003
  bindIp: 192.168.0.33


#operationProfiling:
replication:
    replSetName: shard3
sharding:
    clusterRole: shardsvr

启动三台服务器的shard3 server

numactl --interleave=all mongod  --config  /usr/local/mongodb/conf/shard3.conf

登陆任意一台服务器,初始化副本集

mongo 192.168.0.33:27003
#使用admin数据库
use admin
#定义副本集配置
config = {
...    _id : "shard3",
...     members : [
...         {_id : 0, host : "192.168.0.33:27003" },
...         {_id : 1, host : "192.168.0.34:27003" },
...         {_id : 2, host : "192.168.0.35:27003" }
...     ]
... }


#初始化副本集配置
rs.initiate(config);

#查看分区状态
rs.status();

配置第四个分片副本集

在服务器34、35、31上面做以下配置

配置文件

vi /usr/local/mongodb/conf/shard4.conf

#配置文件内容
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /data/shard4/log/shard4.log

# Where and how to store data.
storage:
  dbPath: /data/shard4/data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 20

# how the process runs
processManagement:
  fork: true 
  pidFilePath: /data/shard4/log/shard4.pid

# network interfaces
net:
  port: 27004
  bindIp: 192.168.0.35


#operationProfiling:
replication:
    replSetName: shard4
sharding:
    clusterRole: shardsvr

启动三台服务器的shard4 server

numactl --interleave=all mongod  --config /usr/local/mongodb/conf/shard4.conf

登陆任意一台服务器,初始化副本集

mongo 192.168.0.34:27004
#使用admin数据库
use admin
#定义副本集配置
config = {
...    _id : "shard4",
...     members : [
...         {_id : 0, host : "192.168.0.34:27004" },
...         {_id : 1, host : "192.168.0.35:27004" },
...         {_id : 2, host : "192.168.0.31:27004" }
...     ]
... }


#初始化副本集配置
rs.initiate(config);

#查看分区状态
rs.status();

配置第五个分片副本集

在服务器35、31、32上面做以下配置

配置文件

vi /usr/local/mongodb/conf/shard5.conf

#配置文件内容
# where to write logging data.
systemLog:
  destination: file
  logAppend: true
  path: /data/shard5/log/shard5.log

# Where and how to store data.
storage:
  dbPath: /data/shard5/data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 20

# how the process runs
processManagement:
  fork: true 
  pidFilePath: /data/shard5/log/shard5.pid

# network interfaces
net:
  port: 27005
  bindIp: 192.168.0.35


#operationProfiling:
replication:
    replSetName: shard5
sharding:
    clusterRole: shardsvr

启动三台服务器的shard5 server

numactl --interleave=all mongod  --config  /usr/local/mongodb/conf/shard5.conf

登陆任意一台服务器,初始化副本集

mongo 192.168.0.35:27005
#使用admin数据库
use admin
#定义副本集配置
config = {
...    _id : "shard5",
...     members : [
...         {_id : 0, host : "192.168.0.35:27005" },
...         {_id : 1, host : "192.168.0.31:27005" },
...         {_id : 2, host : "192.168.0.32:27005" }
...     ]
... }


#初始化副本集配置
rs.initiate(config);

#查看分区状态
rs.status();

至此,五个分片和副本集搭建完毕

3、配置路由服务器 mongos

以下配置在服务器31、32上执行

注意:先启动配置服务器和分片服务器,后启动路由实例

vi /usr/local/mongodb/conf/mongos.conf

systemLog:
  destination: file
  logAppend: true
  path: /data/mongos/log/mongos.log
processManagement:
  fork: true
#  pidFilePath: /usr/local/mongodb/mongos.pid

# network interfaces
net:
  port: 20000
  bindIp: 192.168.0.31
#监听的配置服务器,只能有1个或者3个 configs为配置服务器的副本集名字
sharding:
   configDB: configs/192.168.0.33:21000,192.168.0.34:21000,192.168.0.35:21000

启动二台服务器的mongos server

mongos  --config  /usr/local/mongodb/conf/mongos.conf

4、启用分片

目前搭建了mongodb配置服务器、路由服务器,各个分片服务器,不过应用程序连接到mongos路由服务器并不能使用分片机制,还需要在程序里设置分片配置,让分片生效。

登陆任意一台mongos

mongo 192.168.0.31:20000
#使用admin数据库
use  admin
#串联路由服务器与分配副本集
sh.addShard("shard1/192.168.0.31:27001,192.168.0.32:27001,192.168.0.33:27001")
sh.addShard("shard2/192.168.0.32:27002,192.168.0.33:27002,192.168.0.34:27002")
sh.addShard("shard3/192.168.0.33:27003,192.168.0.34:27003,192.168.0.35:27003")
sh.addShard("shard4/192.168.0.34:27004,192.168.0.35:27004,192.168.0.31:27004")
sh.addShard("shard5/192.168.0.35:27005,192.168.0.31:27005,192.168.0.32:27005")
#查看集群状态
sh.status()

这样mongodb的五台集群搭建就已经完成了,后期如何优化和运营请查看下一篇文章。

四、错误

rs.initiate报错

执行 rs.initiate(config); 报错:

rs.initiate(config);
{
        "ok" : 0,
        "errmsg" : "No host described in new configuration 1 for replica set shard1 maps to this node",
        "code" : 93,
        "codeName" : "InvalidReplicaSetConfig"
}

最后发现是自己的一个端口号写错了。

启动mongos报错

启动mongos的时候报错:

about to fork child process, waiting until server is ready for connections.
forked process: 1436
ERROR: child process failed, exited with error number 1

这个问题卡了我们半天,找了很多的资料,不是说清理lock文件,就是说清理log文件总无解,最后看到这个网站的提示

ERROR: child process failed, exited with error number 1

去掉了配置文件中 –fork,才将真正的错误日志打印了出来,是我们的配置文件中的路径写错了,本来是log写成了logs

原来:path: /data/logs/mongos.log

改为:path: /data/log/mongos.log

成功

为了方便大家拿取配置文件,我在github上面放置了一份: https://github.com/ityouknow/hadoop-ecosystem-examples/tree/master/mongodb/mongodb-five-cluster-conf

Ubuntu 16.04 LTS 安装Mongodb 3.4

第一步:安装

未分类

未分类

未分类

#setp 1. Import the public key used by the package management system.
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6

未分类

#step 2. Create a list file for MongoDB
echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list

未分类

#step 3. Reload local package database
sudo apt-get update

未分类

#step 4. Install the latest stable version of MongoDB
sudo apt-get install -y mongodb-org

未分类

第二步:启动服务

启动mongodb服务,默认安装后,是启动mongodb服务的

sudo service mongod stop  #停止服务
sudo service mongod start  #启动服务
sudo service mongod restart #重新启动服务
sudo service mongod status #查看状态

未分类

允许开机启动,默认当重启服务器后,mongodb服务会停止,需要设置开机启动mongodb服务

sudo systemctl enable mongod

未分类

第三步:连接

本机连接至mongodb服务,使用mongo命令连接

mongo

未分类

mongodb配置文件

less /etc/mongod.conf

未分类

客户端连接,mongo安装完成后,默认是只能在本机连接,在服务器外部是不能连接mongo的

#切换至root用户
sudo -i
#修改mongo配置文件
vim /etc/mongod.conf

未分类

修改完成后,保存文件,并重启mongo

#退出root用户
exit
#重启服务
sudo service mongod restart

客户端连接成功

未分类

第四步:删除mongodb

请参考文档《Install MongoDB Community Edition on Ubuntu》 (https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/#uninstall-mongodb-community-edition)

#停止mongodb服务
sudo service mongod stop
#删除包
sudo apt-get purge mongodb-org*
#删除数据文件及日志文件
sudo rm -r /var/log/mongodb
sudo rm -r /var/lib/mongodb

mongodb日志存储优化

背景

之前写的代码所有的日志都通过mongodb来存储和检索,目前硬盘200G已经使用了195G,监控一直报警。一开始通过精简已有日志中的字段,发现空间下降不明显。于是翻了下官方手册,刚好翻到mongodb的备份,想了想历史日志也用不着及时搜索,于是就备份了

备份命令

mongodump --archive=xxxx.archive --db db --collection collection

压缩备份

gzip xxxx.archive

写个脚本删除备份的日志

后续

备份恢复命令

mongorestore --gzip --archive=xxxx.archive.gz --db db

shell脚本

由于我的日志是按照日期分天存储,因此比较好导出,脚本如下

#!/bin/bash
a="mongodump --archive="
b=".archive"
c="--db php_log --collection "
for i in $( seq $1 $2 )
do
    collection="$3_$i"
    archive="$collection$b"
    $a$archive $c$collection
    gzip $archive
done

脚本比较烂,不要嘲笑????

删除1/2日志成果

删除前:剩余5G 删除后:剩余95G 数据压缩包:5G 节约空间90G,不得不说mongo很吃硬盘啊(更吃内存)

Mongodb开启身份验证

1. 介绍

不管数据库是在多安全的环境或者本地环境,给数据库建立一个安全的环境是很有必要的。

Mongodb提供了一系列的安全功能,这里介绍一种很常用的身份验证方式。

2. 开启验证

默认情况下,只要在启动数据库的时候没有加上–auth选项,就是没有身份验证功能的,所有客户端都可以进行所有权限的操作。

如果加上过后,我们就可以通过安全的身份验证连接数据库。如果要在数据库中进行身份验证,可以通过db.auth(username, password),如果验证成功则返回1,反之。

3. 建立用户

建立用户我们可以通过db.createUser()方法来建立用户,比如下面这样:

db.createUser({user: 'username', pwd: 'password', roles: [
    {role: 'read', db: 'test'}
]});

db.createUser方法的接受一个对象,里面的user代表用户名,pwd代表密码,而roles是一个数组可以接受多个对象,每个对象可以对应作用于的数据库,其中的role字段代表对作用的数据库的权限,官方规定了一些列的内置角色,可以通过文档查询。

4. 删除用户

删除用户需要具有权限的用户进行操作,通过db.dropUser()方法进行,接受一个字符串,这个字符串就是用户名:

db.dropUser('user1');

5. 获取用户

可以通过db.getUser()方法来获取用户信息,同样它接受一个字符串,字符串为用户名:

db.getUser('user1');

Mongodb主从同步及问题处理

第一步:我们把mongodb部署多服务器上10.12.0.3和10.14.0.1。

第二步:启动10.12.0.3上的mongodb,把该数据库指定为主数据库

先启动主:

mongod --port 25019 --fork --logpath /var/log/mongo/mongdb1.log --dbpath /data/db/  --master

再启动从:

mongod --slave --source 10.12.0.3:25019 --dbpath /opt/product/mongodb/data

未分类

出现了syncing from host:10.12.0.3:25019说明已经从主数据库复制完成了。

常见问题处理:

问题一:从服务器执行同步,报错:errmsg: “not authorized on admin to execute command”

未分类

原因: 主启动携带了–auth .mongo默认是不鉴权。去掉后问题解决。

问题二:[replslave] –source 192.168.1.32:25019 != 19.168.1.30:25019 from local.sources collection

未分类

原因:在一开始的时候我们已经为slave指定了master的host和port,这个会插入到local.sources 这个集合的。所以,把master端口改成10000就可以了。