在开发人员眼中使用数据库



使用数据库开发新功能时,开发周期通常包括(但不限于)以下阶段:



编写SQL迁移→编写代码→测试→发布→监视。



在本文中,我想分享一些实用的建议,说明如何减少每个阶段的时间,而不是降低质量,而是提高质量。 



由于我们使用公司的PostgreSQL并用Java编写服务器代码,因此示例将基于此堆栈,尽管大多数想法并不取决于所使用的数据库和编程语言。



SQL迁移



设计后的开发的第一阶段是编写SQL迁移。主要建议-请勿对数据架构进行任何手动更改,但始终通过脚本进行更改并将其存储在一个位置。 



在我们公司,开发人员自己编写SQL迁移,因此所有迁移都与主要代码一起存储在存储库中。在某些公司中,数据库管理员会参与更改架构的工作,在这种情况下,迁移注册表会随处可见。这种方法以一种方式或其他方式具有以下优点:



  • 您始终可以轻松地从头开始创建新基础,也可以将现有基础升级到当前版本。这使您可以快速部署新的测试环境和本地开发环境。
  • 所有基地的布局都相同-服务不会令人惊讶。
  • 有所有更改(版本)的历史记录。


有自动化这一过程中,商业和自由很多现成的工具:飞路liquibasesqitch等。在这篇文章中,我不会比较和选择的最佳工具-这是一个独立的大主题,你可以在它身上找到许多文章... 



我们使用飞行通道,因此这里有一些信息:



  • 有两种类型的迁移:基于SQL基于java
  • SQL迁移是不可变的(不可变的)。第一次执行后,无法更改SQL迁移。Flyway为迁移文件的内容计算校验和,并在每次运行时对其进行验证。为了使Java迁移不可变,需要进行其他手动操作
  • flyway_schema_history ( schema_version). , , , .


根据我们的内部协议,所有数据模式更改仅通过SQL迁移进行。它们的不变性确保我们始终可以获得与所有环境完全相同的实际架构。 当无法使用纯SQL编写时,



Java迁移仅用于DML。对于我们来说,这种情况的典型示例是从另一个数据库将数据传输到Postgres的迁移(我们正在从Redis迁移到Postgres,但这是完全不同的故事)。另一个示例是更新大表的数据,该更新在多个事务中执行以最小化表锁定时间。值得一提的是,从Postgres的第11版开始,可以使用plpgsql中的SQL过程来完成此操作。



当Java代码过时时,可以删除迁移以免产生遗留问题(Java迁移类本身保留,但内部是空的)。在我们的国家/地区,这种情况最多可能会在过渡到生产后的一个月内发生-我们认为这是足够的时间来更新所有测试环境和本地开发环境。应当注意,由于Java迁移仅用于DML,因此将其删除不会以任何方式影响从头开始创建新数据库。



对于使用pg_bouncer的用户而言,重要的细微差别



Flyway在迁移过程中应用了锁定,以防止同时执行多个迁移。简化后,它的工作方式如下:



  • 发生锁定捕获 
  • 在单独的事务中执行迁移
  • 畅通无阻。 


对于Postgres,它在会话模式下使用咨询锁,这意味着要使其正常工作,必须在捕获和释放锁的过程中在同一个连接上运行应用程序服务器。如果在事务模式(最常见)或单请求模式下使用pg_bouncer ,则对于每个事务,它可能会返回一个新的连接,并且flyway将无法释放已建立的锁。 



为了解决这个问题,我们在会话模式下在pg_bouncer上使用了一个单独的小型连接池,该池仅用于迁移。从应用程序的角度来看,还有一个单独的池,其中包含1个连接,并且在迁移后通过超时将其关闭,以免浪费资源。



编码



迁移已创建,现在我们正在编写代码。



从应用程序端使用三种方法来处理数据库:



  • 使用ORM(如果我们谈论Java,那么休眠实际上就是标准)
  • 使用普通的SQL + jdbcTemplate等
  • 使用DSL库。


使用ORM可以减少对SQL知识的需求-自动生成很多信息: 

  • 数据模式可以从代码中可用的xml描述或Java实体创建
  • 使用声明性描述定义对象关系-ORM将为您进行联接
  • 当使用Spring Data JPA时,存储库方法signature也可以自动生成更多棘手的查询


另一个“奖励”是开箱即用的数据缓存的存在(对于休眠模式,这是3级缓存)。



但是,需要特别注意的是,ORM与其他任何强大的工具一样,在使用时也需要一定的资格。没有适当的配置,代码很可能会工作,但远非最佳。



相反的是手工编写SQL。这使您可以完全控制请求-确切地执行了您编写的内容,毫不奇怪。但是,显然,这增加了体力劳动量,并增加了对开发人员资格的要求。



DSL库



在这些方法之间大约中间还有另一种方法,其中包括使用DSL库(jOOQQuerydsl等)。它们通常比ORM轻得多,但是比完全手动的数据库工作更方便。DSL的使用不太普遍,因此本文将简要介绍这种方法。 



我们将讨论一种库-jOOQ她提供什么:



  • 数据库检查和类的自动生成
  • 编写请求的流利API。


jOOQ不是ORM-没有自动生成查询或缓存的功能,但与此同时,完全手动方法的一些问题也已解决:

  • 表,视图,函数等的类。数据库对象自动生成 
  • 查询是用Java编写的,这保证了类型的安全性-语法错误的查询或带有错误类型的参数的查询将不会编译-您的IDE将立即提示您输入错误,并且您不必花费时间启动应用程序来检查查询的正确性。这样可以加快开发过程并减少出错的可能性。


在代码中,请求看起来像这样



BookRecord book = dslContext.selectFrom(BOOK)
                        .where(BOOK.LANGUAGE.eq("DE"))
                        .orderBy(BOOK.TITLE)
                        .fetchAny();


如果需要,可以使用纯SQL:



Result<Record> records = dslContext.fetch("SELECT * FROM BOOK WHERE LANGUAGE = ? ORDER BY TITLE LIMIT 1", "DE");


显然,在这种情况下,查询的正确性和结果分析完全在您的肩上。



jOOQ记录和POJO



上例中的BookRecord是书表中一行的包装,并实现了活动记录模式。由于此类是数据访问层的一部分(除了其特定的实现),因此您可能不希望将其传输到应用程序的其他层,而是使用自己的某种pojo对象。为了方便转换记录<–> pojo jooq提供了几种机制:自动手动。上面的链接的文档提供了多种阅读示例,但没有用于插入新数据和更新的示例。让我们填补这个空白: 



private static final RecordUnmapper<Book, BookRecord> unmapper = 
    book -> new BookRecord(book.getTitle(), ...); // - 

public void create(Book book) {
    context.insertInto(BOOK)
            .set(unmapper.unmap(book))
            .execute();
}


如您所见,一切都很简单。



这种方法使您可以在数据访问层类中隐藏实现细节,并避免“泄漏”到应用程序的其他层。 



并且jooq可以使用一组基本方法生成DAO类,以简化表数据的使用并减少手动代码的数量(这与Spring Data JPA非常相似):



public interface DAO<R extends TableRecord<R>, P, T> {
    void insert(P object) throws DataAccessException;    
    void update(P object) throws DataAccessException;
    void delete(P... objects) throws DataAccessException;
    void deleteById(T... ids) throws DataAccessException;
    boolean exists(P object) throws DataAccessException;
    ...
}


在公司中,我们不使用DAO类的自动生成-我们仅对数据库对象生成包装器,并自行编写查询。包装器的生成是在每次重建单独的Maven模块(存储迁移)时发生的。稍后,将详细介绍如何实现此功能。



测试中



编写测试是开发过程中的重要部分-好的测试可以保证代码的质量,并在维护代码的同时节省时间。同时,可以公平地说,反之亦然-错误的测试可能会造成质量代码的错觉,隐藏错误并减慢开发过程。因此,仅仅确定要编写测试是不够的,您需要正确地做同时测试正确性的概念非常模糊,每个人都有自己的一点点。 



测试分类的问题也是如此。本文建议使用以下拆分选项:



  • 单元测试(单元测试) 
  • 整合测试
  • 端到端测试(端到端)。


单元测试包括相互隔离地检查各个模块的功能。模块的大小再次是未定义的,对于某些情况,它是一个单独的方法,对于某些情况,它是一个类。隔离意味着所有其他模块都是模拟或存根(俄语中是模仿或存根,但它们听起来不太好)。单击此链接阅读Martin Fowler关于两者之间区别的文章。单元测试小巧,快速,但是只能保证单个单元逻辑的正确性。



整合测试与单元测试不同,它们检查几个模块之间的交互。当集成测试有意义时,使用数据库是一个很好的例子,因为考虑到数据库的所有细微差别,很难以高质量“锁定”数据库。与其他类型的测试相比,在大多数情况下,集成测试是在测试数据库时在执行速度和质量保证之间做出很好的折衷。因此,在本文中,我们将更详细地讨论这种类型的测试。



端到端测试是最广泛的。为此,必须改善整个环境。它保证了对产品质量的最高信任度,但最慢也最昂贵。



整合测试



当涉及到与数据库一起使用的代码的集成测试时,大多数开发人员会提出以下问题:如何启动数据库,如何使用初始数据初始化其状态,以及如何尽快完成它?



前一段时间,h2是集成测试中相当普遍的做法。它是用Java编写的内存数据库,具有与大多数流行数据库兼容的模式。无需安装数据库并且h2的多功能性使其非常适合替换实际数据库,尤其是在应用程序不依赖于特定数据库并且仅使用SQL标准中包含的内容的情况下(并非总是如此)。 



但是,当您使用一些棘手的数据库功能(或新版本中的全新功能)时,问题就开始了,但在h2中未实现对此功能的支持。通常,由于这是特定DBMS的“模拟”,因此行为上始终可能存在一些差异。



另一种选择是使用嵌入式postgres。这是真正的Postgres,以存档形式提供,不需要安装。它使您可以像常规的Postgres版本一样工作。 



有几种实现,其中最流行的是YandexopenTable... 我们公司中使用了Yandex的版本。缺点-启动非常慢(每次解压缩归档文件并启动数据库时-花费2-5秒,具体取决于计算机的功能),官方发行版的滞后性也存在问题。我们还面临一个问题,即尝试从代码中停止后,发生了一些错误,并且Postgres进程仍然挂在OS中-您必须手动将其杀死。 



测试容器



第三种选择是使用docker。对于Java,有一个testcontainers,它提供了一个API,用于从代码中使用Docker容器。因此,可以在使用testcontainer的测试中替换应用程序中具有Docker映像的任何依赖项。此外,对于许多流行的技术,根据使用的图像,有单独的现成的类提供了更方便的api:



  • 数据库(Postgres,Oracle,Cassandra,MongoDB等), 
  • Nginx的
  • 卡夫卡等


顺便说一句,当tescontainers项目变得非常流行时,yandex开发人员正式宣布他们将停止嵌入式postgres项目的开发,并建议改用testcontainer。



优点是什么:



  • testcontainers很快(启动空的Postgres不到一秒钟)
  • postgres社区为每个新版本发布官方docker映像
  • testcontainers有一个特殊的过程,它会在关闭jvm之后杀死悬空的容器,除非您以编程方式完成了该过程
  • 使用测试容器,您可以使用统一的方法来测试应用程序的外部依赖关系,这显然使事情变得容易。


使用Postgres的 示例测试



@Test
public void testSimple() throws SQLException {
    try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>()) {
        postgres.start();
        ResultSet resultSet = performQuery(postgres, "SELECT 1");
        int resultSetInt = resultSet.getInt(1);
        assertEquals("A basic SELECT query succeeds", 1, resultSetInt);
    }
}


如果在测试容器中没有单独的图像类,则创建一个容器如下所示



public static GenericContainer redis = new GenericContainer("redis:3.0.2")
            .withExposedPorts(6379);


如果您使用的是JUnit4,JUnit5或Spock,则testcontainers还有其他功能。对这些框架的支持,这使得编写测试变得更加容易。



使用测试容器加快测试速度



即使从嵌入式Postgres切换到testcontainers,也可以通过更快地运行Postgres来加快我们的测试速度,但是随着时间的流逝,测试又开始放慢速度。这是由于flyway在启动时执行的SQL迁移数量增加。当迁移数量超过一百时,执行时间约为7-8秒,这大大降低了测试速度。它的工作原理如下:



  1. 在下一个测试课程之前,启动了带有Postgres的“干净”容器
  2. 飞路进行的迁移
  3. 执行了此类的测试
  4. 容器已停下并移走
  5. 从项目1重复下一个测试类别。


显然,随着时间的流逝,第二步花费了越来越多的时间。



为了解决这个问题,我们意识到在所有测试之前仅执行一次迁移就足够了,保存容器的状态然后在所有测试中使用该容器就足够了。因此算法已更改:



  1. 在进行所有测试之前,将启动带有Postgres的“干净”容器
  2. 飞路进行迁移
  3. 容器状态持续
  4. 在下一个测试课程之前,启动先前准备好的容器
  5. 执行此类的测试
  6. 容器停下并移走
  7. 从第4步重复进行下一个测试课程。


现在,单个测试的执行时间不再取决于迁移的数量,而以当前的迁移数量(200+)而言,新方案在所有测试的每次运行中节省了几分钟的时间。



以下是有关如何实现此功能的一些技术细节。



Docker具有内置机制,可使用commit命令从正在运行的容器中创建新映像它允许您自定义图像,例如,通过更改任何设置。 



一个重要的细微差别是该命令不会保存已安装分区的数据。但是,如果您获取正式的Postgres泊坞窗映像,则存储数据的PGDATA目录位于一个单独的部分中(以便在重新启动容器后不会丢失数据),因此,在执行提交时,不会保存数据库本身的状态。 



解决方案很简单-请勿将节用于PGDATA,而是将数据保留在内存中,这对于测试来说是很正常的。有两种方法可以使用-使用您的dockerfile(类似这样)而不创建一个节,或者在启动官方容器时覆盖PGDATA变量(该节将保留,但将不使用)。第二种方法看起来更简单:



PostgreSQLContainer<?> container = ...
container.addEnv("PGDATA", "/var/lib/postgresql/data-no-mounted");
container.start();


提交之前,建议检查点postgres以将更改从共享缓冲区刷新到“磁盘”(对应于重写的PGDATA变量):



container.execInContainer("psql", "-c", "checkpoint");


提交本身如下所示:



CommitCmd cmd = container.getDockerClient().commitCmd(container.getContainerId())
                .withMessage("Container for integration tests. ...")
                .withRepository(imageName)
                .withTag(tag);
String imageId = cmd.exec();


值得注意的是,这种使用准备好的图像的方法可以应用于许多其他图像,这也可以节省运行集成测试时的时间。



关于优化构建时间的几句话



如前所述,在通过迁移组装一个单独的Maven模块时,除此以外,还会在数据库对象上生成Java包装器。为此,使用了一个自写的Maven插件,该插件在编译主代码之前启动并执行3个操作:



  1. 使用postgres运行“干净”的docker容器
  2. 启动Flyway,它对所有数据库执行sql迁移,从而检查其有效性
  3. 运行Jooq,它检查数据库模式并为表,视图,函数和其他模式对象生成Java类。


如您所见,前两个步骤与运行测试时执行的步骤相同。为了节省启动容器和在测试前运行迁移的时间,我们将容器状态的保存移至插件。因此,现在,在重建模块后,用于代码中使用的所有数据库的集成测试的现成映像会立即出现在docker映像的本地存储库中。



更详细的代码示例
@ThreadSafe
public class PostgresContainerAdapter implements PostgresExecutable {
  private static final String ORIGINAL_IMAGE = "postgres:11.6-alpine";

  @GuardedBy("this")
  @Nullable
  private PostgreSQLContainer<?> container; // not null if it is running

  @Override
  public synchronized String start(int port, String db, String user, String password) 
  {
    Preconditions.checkState(container == null, "postgres is already running");

    PostgreSQLContainer<?> newContainer = new PostgreSQLContainer<>(ORIGINAL_IMAGE)
        .withDatabaseName(db)
        .withUsername(user)
        .withPassword(password);

    newContainer.addEnv("PGDATA", "/var/lib/postgresql/data-no-mounted");

    // workaround for using fixed port instead of random one chosen by docker
    List<String> portBindings = new ArrayList<>(newContainer.getPortBindings());
    portBindings.add(String.format("%d:%d", port, POSTGRESQL_PORT));
    newContainer.setPortBindings(portBindings);
    newContainer.start();

    container = newContainer;
    return container.getJdbcUrl();
  }

  @Override
  public synchronized void saveState(String name) {
    try {
      Preconditions.checkState(container != null, "postgres isn't started yet");

      // flush all changes
      doCheckpoint(container);

      commitContainer(container, name);
    } catch (Exception e) {
      stop();
      throw new RuntimeException("Saving postgres container state failed", e);
    }
  }

  @Override
  public synchronized void stop() {
    Preconditions.checkState(container != null, "postgres isn't started yet");

    container.stop();
    container = null;
  }

  private static void doCheckpoint(PostgreSQLContainer<?> container) {
    try {
      container.execInContainer("psql", "-c", "checkpoint");
    } catch (IOException | InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  private static void commitContainer(PostgreSQLContainer<?> container, String image)
  {
    String tag = "latest";
    container.getDockerClient().commitCmd(container.getContainerId())
        .withMessage("Container for integration tests. It uses non default location for PGDATA which is not mounted to a volume")
        .withRepository(image)
        .withTag(tag)
        .exec();
  }
  // ...
}


( «start»):

@Mojo(name = "start")
public class PostgresPluginStartMojo extends AbstractMojo {
  private static final Logger logger = LoggerFactory.getLogger(PostgresPluginStartMojo.class);

  @Nullable
  static PostgresExecutable postgres;

  @Parameter(defaultValue = "5432")
  private int port;
  @Parameter(defaultValue = "dbName")
  private String db;
  @Parameter(defaultValue = "userName")
  private String user;
  @Parameter(defaultValue = "password")
  private String password;

  @Override
  public void execute() throws MojoExecutionException {
    if (postgres != null) { 
      logger.warn("Postgres already started");
      return;
    }
    logger.info("Starting Postgres");
    if (!isDockerInstalled()) {
      throw new IllegalStateException("Docker is not installed");
    }
    String url = start();
    testConnection(url, user, password);
    logger.info("Postgres started at " + url);
  }

  private String start() {
    postgres = new PostgresContainerAdapter();
    return postgres.start(port, db, user, password);
  }

  private static void testConnection(String url, String user, String password) throws MojoExecutionException {
    try (Connection conn = DriverManager.getConnection(url, user, password)) {
      conn.createStatement().execute("SELECT 1");
    } catch (SQLException e) {
      throw new MojoExecutionException("Exception occurred while testing sql connection", e);
    }
  }

  private static boolean isDockerInstalled() {
    if (CommandLine.executableExists("docker")) {
      return true;
    }
    if (CommandLine.executableExists("docker.exe")) {
      return true;
    }
    if (CommandLine.executableExists("docker-machine")) {
      return true;
    }
    if (CommandLine.executableExists("docker-machine.exe")) {
      return true;
    }
    return false;
  }
}


save-state stop .



:



<build>
  <plugins>
    <plugin>
      <groupId>com.miro.maven</groupId>
      <artifactId>PostgresPlugin</artifactId>
      <executions>
        <!-- running a postgres container -->
        <execution>
          <id>start-postgres</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>start</goal>
          </goals>
          
          <configuration>
            <db>${db}</db>
            <user>${dbUser}</user>
            <password>${dbPassword}</password>
            <port>${dbPort}</port>
          </configuration>
        </execution>
        
        <!-- applying migrations and generation java-classes -->
        <execution>
          <id>flyway-and-jooq</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>execute-mojo</goal>
          </goals>
          
          <configuration>
            <plugins>
              <!-- applying migrations -->
              <plugin>
                <groupId>org.flywaydb</groupId>
                <artifactId>flyway-maven-plugin</artifactId>
                <version>${flyway.version}</version>
                <executions>
                  <execution>
                    <id>migration</id>
                    <goals>
                      <goal>migrate</goal>
                    </goals>
                    
                    <configuration>
                      <url>${dbUrl}</url>
                      <user>${dbUser}</user>
                      <password>${dbPassword}</password>
                      <locations>
                        <location>filesystem:src/main/resources/migrations</location>
                      </locations>
                    </configuration>
                  </execution>
                </executions>
              </plugin>

              <!-- generation java-classes -->
              <plugin>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-codegen-maven</artifactId>
                <version>${jooq.version}</version>
                <executions>
                  <execution>
                    <id>jooq-generate-sources</id>
                    <goals>
                      <goal>generate</goal>
                    </goals>
                      
                    <configuration>
                      <jdbc>
                        <url>${dbUrl}</url>
                        <user>${dbUser}</user>
                        <password>${dbPassword}</password>
                      </jdbc>
                      
                      <generator>
                        <database>
                          <name>org.jooq.meta.postgres.PostgresDatabase</name>
                          <includes>.*</includes>
                          <excludes>
                            #exclude flyway tables
                            schema_version | flyway_schema_history
                            # other excludes
                          </excludes>
                          <includePrimaryKeys>true</includePrimaryKeys>
                          <includeUniqueKeys>true</includeUniqueKeys>
                          <includeForeignKeys>true</includeForeignKeys>
                          <includeExcludeColumns>true</includeExcludeColumns>
                        </database>
                        <generate>
                          <interfaces>false</interfaces>
                          <deprecated>false</deprecated>
                          <jpaAnnotations>false</jpaAnnotations>
                          <validationAnnotations>false</validationAnnotations>
                        </generate>
                        <target>
                          <packageName>com.miro.persistence</packageName>
                          <directory>src/main/java</directory>
                        </target>
                      </generator>
                    </configuration>
                  </execution>
                </executions>
              </plugin>
            </plugins>
          </configuration>
        </execution>

        <!-- creation an image for integration tests -->
        <execution>
          <id>save-state-postgres</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>save-state</goal>
          </goals>
          
          <configuration>
            <name>postgres-it</name>
          </configuration>
        </execution>

        <!-- stopping the container -->
        <execution>
          <id>stop-postgres</id>
          <phase>generate-sources</phase>
          <goals>
            <goal>stop</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>




发布



该代码已经过编写和测试-现在该发布了。通常,发布的复杂性取决于以下因素:



  • 数据库数量(一个或多个)
  • 关于数据库的大小
  • 在应用程序服务器的数量(一个或多个)上
  • 是否无缝发布(是否允许应用程序停机)。


第1项和第3项对代码提出了向后兼容性的要求,因为在大多数情况下不可能同时更新所有数据库和所有应用程序服务器-总会有一个时间点,数据库将具有不同的架构,并且服务器将具有不同的代码版本。



数据库的大小会影响迁移时间-数据库越大,您进行长时间迁移的可能性就越大。



无缝性在某种程度上是导致因素的原因-如果发布是在关机(停机)的情况下进行的,那么前3点并不是那么重要,而仅影响应用程序不可用的时间。



如果我们谈论我们的服务,那么这些是:



  • 大约30个数据库集群


  • 一个基础的大小200-400 GB
  • ( 100),
  • .


我们使用Canary版本:该应用程序的新版本首先显示在少量服务器上(我们称其为预发布版本),过一会儿,如果在预发布版本中未发现错误,则会将其发布到其他服务器上。因此,生产服务器可以在不同版本上运行。



启动时,每个应用程序服务器都会使用源代码中的脚本版本检查数据库版本(就飞行方式而言,这称为Validation)。如果它们不同,则服务器将无法启动。这样可以确保代码和数据库的兼容性。例如,当代码与尚未创建的表一起使用时,由于迁移位于服务器的不同版本中,因此不会出现这种情况。



但这当然不能解决问题,例如,在新版本的应用程序中,有一个迁移会删除表中的列,而该列可以在服务器的旧版本中使用。现在,我们仅在审查阶段检查这种情况(这是强制性的),但是以友好的方式,有必要引入其他情况。在CI / CD循环中进行此类检查。  



有时迁移会花费很长时间(例如,从大表中更新数据时),并且为了不减慢发布速度,我们使用组合迁移技术... 组合包括在运行中的服务器上手动运行迁移(通过管理面板,没有运行通道,因此没有记录在迁移历史中),然后在下一版本的服务器中“常规”输出相同的迁移。这些迁移必须满足以下要求:



  • 首先,应以在长时间执行过程中不阻塞应用程序的方式编写(此处的重点不是在数据库级别获取长期锁定)。为此,我们为开发人员提供了有关如何编写迁移的内部准则。将来,我可能还会在Habré上分享它们。
  • 其次,在“常规”开始时进行的迁移应确定它已在手动模式下执行,并且在这种情况下不执行任何操作-仅提交历史记录中的新记录。对于SQL迁移,通过执行一些SQL查询更改来执行这种检查。Java迁移的另一种方法是使用存储的布尔标志,这些标志是在手动运行后设置的。




此方法解决了两个问题:

  • 发布很快(尽管有手动操作)
  • ( ) - .




一旦发布,开发周期就不会结束。为了了解新功能是否有效(以及如何工作),有必要将度量“封闭”。它们可以分为2组:业务和系统。 



第一组在很大程度上取决于主题领域:对于邮件服务器,了解发送的信件数量,新闻资源-每天的唯一身份用户数量等很有用。



第二组的指标对于每个人来说都大致相同-它们确定服务器的技术状态:cpu,内存,网络,数据库等 



。究竟需要监视什么以及如何对其进行监视,是大量不同文章的主题,在此不再赘述。我只想提醒最基本的(甚至是队长):



预先定义指标



有必要定义一个基本指标列表。当您不了解系统正在发生什么时,应该在发布之前而不是在第一次事件发生之前提前进行。



设置自动警报



这将加快您的反应时间并节省手动监视的时间。理想情况下,您应该在用户感觉到问题并写信给您之前先了解问题。



从所有节点收集指标



像日志这样的指标永远不会太多。系统每个节点(应用程序服务器,数据库,连接器,平衡器等)中的数据都可以使您全面了解其状态,并且在必要时可以快速定位问题。 



一个简单的例子:在网页上加载数据开始变慢。原因可能有很多:



  • Web服务器超载,需要很长时间才能响应请求


  • SQL查询需要更长的时间才能执行
  • 连接池上已累积了一个队列,并且应用程序服务器长时间无法接收连接
  • 网络问题
  • 其他的东西


没有度量标准,找到问题的根本原因将不容易。



而不是完成



我想说一个平庸的说法,那就是没有灵丹妙药,选择一种或另一种方法取决于特定任务的要求,而对他人有效的方法可能不适用于您。但是,您知道的方法越不同,您就可以做出越彻底和定性的选择。我希望您从这篇文章中学到了一些对自己有帮助的新东西,将来对您有帮助。我很乐于评论您使用什么方法来改进数据库的工作过程。



All Articles