8

我们有一个使用 Microsoft SQL Server 作为数据库后端的企业应用程序。我们遇到过一些例子,客户将应用程序发展成一个巨大的数据库,并且一些正在运行的查询导致他们自己和其他用户出现锁定和性能问题。

我们已尝试应用尽可能多的索引,并将所有查询调整到极限,但我们有一个应用程序必须适合许多不同的客户类型,因此很难创建一种适合所有人的解决方案。我们没有资源来应用每个客户的客户特定索引/性能。

我们知道导致问题的主要查询是为驱动报告和 kpi 而生成的查询。

我的问题是,有没有一种方法可以分散应用程序的负载,以便日常使用不会受到报告/kpi 生成的阻碍。即我们能否以某种方式镜像/复制数据库,以便将日常操作发送到 SQL 实体 A,并将数据密集型查询发送到 SQL 实体 B?因此,数据密集型查询对日常事务没有影响,我们可以将查询排队到 SQL 实体 B。

在这种情况下,SQL 实体 A 和 B 需要始终保持对齐,但 SQL 实体 B 将始终是只读的。

任何人都可以建议我们可以尝试实现这一目标的任何途径吗?或者我应该考虑另一种方法来让我们在性能上获胜。

谢谢

4

6 回答 6

4

看来您可以使用任何复制选项并且没问题。在我之前的一项工作中,我们为此使用了 Log Shipping http://technet.microsoft.com/en-us/library/ms187103.aspx 。

您还可以处理复制类型:http ://technet.microsoft.com/en-us/library/bb677158.aspx ,看看哪一个最适合您,因为您可以做的不仅仅是在辅助数据库上报告。

如果我没记错我最初的经验,日志传送非常容易设置,所以你可能想从那里开始。

于 2013-10-09T09:20:33.273 回答
3

啊...性能调整 SQL Server 等。人。我最喜欢的东西!

任何人都可以建议我们可以尝试实现这一目标的任何途径吗?

根据您提供的信息,我将对数据进行垂直分区。这意味着为实际的 OLTP(CRUD 事务)和 KPI(服务器 B)维护一个数据库(服务器 A)。

对于复制,我将使用事务复制 - 正确执行时,延迟将低于一秒。我想不出这是不合适的实际场景。事实上,大多数报告都是在前一天结束时完成的,“实时”通常意味着最后 5 分钟

为了管理复制过程,我将从一个简单的控制台应用程序开始,期望在适当的时候扩展它以满足需求。控制台应用程序应使用以下命名空间(重新考虑后可能有可用于 SQL2012 的命名空间)

using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Replication;

使用控制台应用程序,您可以在单个界面中管理发布、订阅和任何跟踪令牌。配置(所有这些权限、密码和路径)将是一个痛苦的过程,但是一旦它启动并运行,您将能够优化数据的事务数据库和用于...报告的报告服务器。

我将有一个复制拓扑,对于大表来说,每个表实际上是一个订阅,其余的(查找表、视图 sp)只有一个订阅。我会复制主键,但不会复制约束、表引用、触发器(依赖源数据库来保证完整性)。您也不应该复制索引 - 它们可以为报告服务器手动配置/优化。

您还可以选择适合 KPI 的文章,即(无需复制文本、varchar(max) 等)

下面发布了一些帮助功能,以帮助您进行操作。

还是我应该考虑另一种方法来让我们在性能上获胜?

以我卑微的经验,总有一些事情可以做来提高性能。它归结为时间->成本->收益。有时在功能上稍作妥协会给您带来很多性能优势。

魔鬼在细节中,但有一个警告......

进一步的随机想法

您已经确定了您的基础架构问题之一 - 混合 OLTP 和 BI/报告。我不清楚您的经验以及您的性能问题有多糟糕,而复制绝对是正确的方法,如果您处于“灭火”模式,您可以尝试。

  1. 数据库或 RAM 中 KPI 结果(5 分钟、1 小时、1 天?)的服务器端缓存
  2. 使用模式绑定视图,您可以创建索引视图(在标准版和企业版上)取决于 KPI 的类型 - 这甚至可能是您需要做的所有事情!请参阅http://msdn.microsoft.com/en-us/library/ms191432.aspx了解更多信息。从本质上讲,如果您的 KPI 是总和/分组依据,您应该好好看看。
  3. 为每日 KPI 预先计算总和。然后,您可以选择仅添加当前日期的数据。
  4. 排序 - 由于order by条款,KPI 是否昂贵。确保您的聚集索引是正确的(REM:它们不需要存在于主键上)。获得数据后尝试在客户端上进行排序
  5. 聚集索引大小。越小越好。如果您使用 GUID,请从这里开始
  6. 垂直分区数据 - 例如,如果您有一个包含 200 列的表,但 KPI 仅使用 10 列 - 将 10 列放入不同的表中 - 您将在每次 i/o 页面读取时获得更多数据(如果您的磁盘是瓶颈)
  7. 提供“通过电子邮件发送报告”的功能——去掉实时性。当事情比较安静并且白天的“实时”报告数量较少时,您可能能够在一夜之间提供一定百分比的报告。一些客户实际上可能更喜欢此功能
  8. 让您的客户为报告付费!“只需在此处输入您的信用卡详细信息......”这是减少报告数量的可靠方法 :)

有关您的配置的更多信息会很有用 - 当您说巨大时,有多大?有多大/什么类型的磁盘,RAM 规格是什么等 - 我问的原因是这个......你可能会花费接下来的 40 个工作日(每天 500 美元以上?)调整 - 这会给你带来很多硬件!- 更多 RAM、更多磁盘、更快的磁盘 - 用于 tempdb 或索引分区的 SSD。换句话说……您可能对硬件要求太多(而您的老板对您要求太多)

接下来,您描述一个企业应用程序,这是一个 Enterprise SQL Server 许可证。如果是这样,您很幸运 - 您可以创建模式绑定分区视图并将查询委托给“正确”服务器。虽然这个模型存在一些问题 - 即加入,但它确实为您提供了一个有效的替代选项。

复制代码

我知道我在某处拥有它。在下面找到一些 RMO 辅助函数,您可能会发现它们对开始复制很有用。在过去的某个时候,它是实时代码,但可能比我想的要早 - 请视为伪代码。

(PS 很高兴如果您愿意,可以直接与您取得联系)

 public static class RMOHelper
    {
        public static void PreparePublicationDb(MyServer Src, MyServer Dist)
        {
            ReplicationDatabase publicationDb = new ReplicationDatabase(Src.Database, Src.ServerConnection);
            if (publicationDb.LoadProperties())
            {
                if (!publicationDb.EnabledTransPublishing)
                {
                    publicationDb.EnabledTransPublishing = true;
                }

                // If the Log Reader Agent does not exist, create it.
                if (!publicationDb.LogReaderAgentExists)
                {
                    // Specify the Windows account under which the agent job runs.
                    // This account will be used for the local connection to the 
                    // Distributor and all agent connections that use Windows Authentication.
                    publicationDb.LogReaderAgentProcessSecurity.Login = Dist.WinUId;
                    publicationDb.LogReaderAgentProcessSecurity.Password = Dist.WinPwd;

                    // Explicitly set authentication mode for the Publisher connection
                    // to the default value of Windows Authentication.
                    publicationDb.LogReaderAgentPublisherSecurity.WindowsAuthentication = true;

                    // Create the Log Reader Agent job.
                    publicationDb.CreateLogReaderAgent();
                    DeleteJobAgentSchedule(publicationDb.LogReaderAgentName);
                }
            }
            else
            {
                throw new ApplicationException(String.Format(
                    "The {0} database does not exist at {1}.",
                    publicationDb,
                    Src.ServerName));
            }
        }
        public static TransPublication PrepareTransPublication(MyServer Src, MyServer Dist, string publicationName)
        {
            // Set the required properties for the transactional publication.
            TransPublication publication = new TransPublication();
            publication.ConnectionContext = Src.ServerConnection;
            publication.Name = publicationName;
            publication.DatabaseName = Src.Database;
            if (publicationName == "relation")
            {
                float d = 0;

            }

            // Specify a transactional publication (the default).
            publication.Type = PublicationType.Transactional;
            publication.ConflictRetention = 4;
            publication.RetentionPeriod = 72;
            // Activate the publication so that we can add subscriptions.
            publication.Status = State.Active;
            // Enable push and pull subscriptions and independent Distribition Agents.
            publication.Attributes = PublicationAttributes.AllowPull|PublicationAttributes.AllowPush|PublicationAttributes.IndependentAgent;
            //publication.Attributes &= PublicationAttributes.AllowSyncToAlternate;
            // Specify the Windows account under which the Snapshot Agent job runs.
            // This account will be used for the local connection to the 
            // Distributor and all agent connections that use Windows Authentication.
            publication.SnapshotGenerationAgentProcessSecurity.Login = Dist.WinUId;
            publication.SnapshotGenerationAgentProcessSecurity.Password = Dist.WinPwd;

            // Explicitly set the security mode for the Publisher connection
            // Windows Authentication (the default).
            publication.SnapshotGenerationAgentPublisherSecurity.WindowsAuthentication = true;
            publication.SnapshotGenerationAgentProcessSecurity.Login =Dist.WinUId;
            publication.SnapshotGenerationAgentProcessSecurity.Password =  Dist.WinPwd;

            publication.AltSnapshotFolder = @"\\192.168.35.4\repldata\";
            if (!publication.IsExistingObject)
            {
                // Create the transactional publication.
                publication.Create();

                // Create a Snapshot Agent job for the publication.
                publication.CreateSnapshotAgent();
                // DeleteJobAgentSchedule(ByVal jobID As Guid) As Boolean
            }
            else
            {
                //throw new ApplicationException(String.Format(
                //    "The {0} publication already exists.", publicationName));
            }
            return publication;
        }
        public static TransArticle PrepareTransArticle(TransPublication TransPub, Happy.MI.Replication.Article Article)
        {
            TransArticle TransArticle = new TransArticle();
            TransArticle.ConnectionContext = TransPub.ConnectionContext;
            TransArticle.Name = Article.Name;
            TransArticle.DatabaseName = TransPub.DatabaseName;
            TransArticle.SourceObjectName = Article.Name;
            TransArticle.SourceObjectOwner = "dbo";
            TransArticle.PublicationName = TransPub.Name;
            //article.Type = ArticleOptions.LogBased;
            //article.FilterClause = "DiscontinuedDate IS NULL";

            // Ensure that we create the schema owner at the Subscriber.


            if (TransArticle.IsExistingObject)
            {
                //do somethinbg??
            }
            else
            {

                TransArticle.SchemaOption |= CreationScriptOptions.Schema;
                TransArticle.SchemaOption |= CreationScriptOptions.AttemptToDropNonArticleDependencies;

                if (!Article.ObjectType.HasValue)
                {
                    throw new Exception(string.Format("unknown schema object type for trans article {0}", Article.Name));
                }
                if (Article.ObjectType.Value== DataAccessADO.ObjectType.USER_TABLE)
                {
                    TransArticle.SchemaOption |= CreationScriptOptions.ClusteredIndexes;
                    TransArticle.SchemaOption |= CreationScriptOptions.DriChecks;
                    TransArticle.SchemaOption |= CreationScriptOptions.DriDefaults;
                    TransArticle.SchemaOption |= CreationScriptOptions.DriPrimaryKey;
                    TransArticle.SchemaOption |= CreationScriptOptions.DriUniqueKeys;
                    //TransArticle.SchemaOption |= CreationScriptOptions.ExtendedProperties;
                    //TransArticle.SchemaOption |= CreationScriptOptions.NonClusteredIndexes;
                    TransArticle.Type = ArticleOptions.LogBased;
                    TransArticle.AddReplicatedColumns(Article.IncludedColumns.ToArray());
                }
                else if (Article.ObjectType.Value == DataAccessADO.ObjectType.VIEW)
                {
                    TransArticle.Type= ArticleOptions.ViewSchemaOnly;
                }
                else if (Article.ObjectType.Value == DataAccessADO.ObjectType.SQL_SCALAR_FUNCTION)
                {
                    TransArticle.Type = ArticleOptions.FunctionSchemaOnly;
                }
                else if (Article.ObjectType.Value == DataAccessADO.ObjectType.SQL_STORED_PROCEDURE)
                {
                    TransArticle.Type = ArticleOptions.ProcSchemaOnly;
                }
                else
                {
                    throw new Exception(string.Format("unsupported schema object type {0}", Article.ObjectType.Value));
                }
                // Create the article.
                TransArticle.Create();
            }
            return TransArticle;
        }
        public static TransSubscription PrepareSubscription(TransPublication TransPub, MyServer Src, MyServer Dest, MyServer Dist)
        {
            // Define the push subscription.
            //TransPullSubscription subscription = new TransPullSubscription();
            //subscription.ConnectionContext =  Dest.ServerConnection;
            //subscription.PublisherName = Src.ServerName;
            //subscription.PublicationName = TransPub.Name;
            //subscription.PublicationDBName = Src.Database;
            //subscription.DatabaseName = Dest.Database;
            TransSubscription subscription = new TransSubscription();
            subscription.ConnectionContext = TransPub.ConnectionContext;
            subscription.PublicationName = TransPub.Name;
            subscription.DatabaseName =  TransPub.DatabaseName;
            subscription.SubscriptionDBName = Dest.Database;
            subscription.SubscriberName = Dest.ServerName;
            subscription.LoadProperties();

            //subscription.Remove();

            // Specify the Windows login credentials for the Distribution Agent job.
            subscription.SynchronizationAgentProcessSecurity.Login = Dist.WinUId;
            subscription.SynchronizationAgentProcessSecurity.Password = Dist.WinPwd;


            if(!subscription.IsExistingObject){


                // Create the push subscription.


                // By default, subscriptions to transactional publications are synchronized 
                // continuously, but in this case we only want to synchronize on demand.
                subscription.AgentSchedule.FrequencyType = ScheduleFrequencyType.Continuously;

                subscription.Create();

                PrepareSnapshot(TransPub, Src, Dist);
            }
            return subscription;
        }
        public static void PrepareSnapshot(TransPublication TPub, MyServer Src, MyServer Dist)
        {
            SnapshotGenerationAgent agent = new SnapshotGenerationAgent();
            agent.Distributor = Dist.ServerName;
            agent.DistributorSecurityMode = SecurityMode.Standard;
            agent.DistributorLogin = Dist.SQLUId;
            agent.DistributorPassword = Dist.WinPwd;

            agent.Publisher = TPub.SqlServerName;
            agent.PublisherSecurityMode = SecurityMode.Standard;
            agent.PublisherLogin = Src.SQLUId;
            agent.PublisherPassword = Src.SQLPwd;

            agent.Publication = TPub.Name;
            agent.PublisherDatabase = TPub.DatabaseName;
            agent.ReplicationType = ReplicationType.Transactional;

            // Start the agent synchronously.
            agent.GenerateSnapshot();

        }
        public static void ApplySubscription(Happy.MI.Replication.Subscription _subscription)
        {

            Happy.MI.Replication.Publication p = _subscription.Publication;
            RMOHelper.PreparePublicationDb(_subscription.Publication.Src, _subscription.Publication.Dist);
            TransPublication TransPub = RMOHelper.PrepareTransPublication(p.Src, p.Dist, p.PublicationName);
            foreach (Happy.MI.Replication.Article a in p.Articles)
            {
                a.LoadProperties();
                TransArticle ta = RMOHelper.PrepareTransArticle(TransPub, a);
                ta.ConnectionContext.Disconnect();

            }
            TransSubscription TransSub = RMOHelper.PrepareSubscription(TransPub, p.Src, _subscription.Dest, p.Dist);
            if (TransSub.LoadProperties() && TransSub.AgentJobId == null)
            {
                // Start the Distribution Agent asynchronously.
                TransSub.SynchronizeWithJob();

            }
            TransSub.ConnectionContext.Disconnect();

            //foreach (Happy.MI.Replication.Subscription s in p.Subscriptions)
            //{
            //    TransSubscription TransSub = RMOHelper.PrepareSubscription(TransPub, p.Src, s.Dest, p.Dist);

            //    if (TransSub.LoadProperties() && TransSub.AgentJobId == null)
            //    {
            //        // Start the Distribution Agent asynchronously.
            //        TransSub.SynchronizeWithJob();

            //    }
            //    TransSub.ConnectionContext.Disconnect();
            //}
            //TransPub.ConnectionContext.Disconnect();

        }
        public static void Create(Happy.MI.Replication.Publication p)
        {
            RMOHelper.PreparePublicationDb(p.Src, p.Dist);

            TransPublication TransPub = RMOHelper.PrepareTransPublication(p.Src, p.Dist, p.PublicationName);
            foreach (Happy.MI.Replication.Article a in p.Articles)
            {
                a.LoadProperties();
                RMOHelper.PrepareTransArticle(TransPub, a);
            }
            foreach (Happy.MI.Replication.Subscription s in p.Subscriptions)
            {
                TransSubscription TransSub = RMOHelper.PrepareSubscription(TransPub, p.Src, s.Dest, p.Dist);

                if (TransSub.LoadProperties() && TransSub.AgentJobId == null)
                {
                    // Start the Distribution Agent asynchronously.
                    TransSub.SynchronizeWithJob();

                }
            }
        }
        private static  void DeleteJobAgentSchedule(string s)
        {
            //    Private Function DeleteSchedule(ByVal scheduleID As Integer) As Boolean
            //    Dim result As Boolean

            //    If (scheduleID > 0) Then
            //        Dim msdbConnectionString As String = Me.PublicationConnectionString.Replace(String.Format("Initial Catalog={0};", Me.PublicationDbName), "Initial Catalog=msdb;")

            //        Dim db As New SQLDataAccessHelper.DBObject(msdbConnectionString)

            //        '-- Delete Job Schedule
            //        Dim parameters As New List(Of System.Data.SqlClient.SqlParameter)

            //        parameters.Add(New System.Data.SqlClient.SqlParameter("@schedule_id", SqlDbType.Int))
            //        parameters.Add(New System.Data.SqlClient.SqlParameter("@force_delete", SqlDbType.Bit))

            //        parameters(0).Value = scheduleID
            //        parameters(1).Value = True

            //        Dim rowsAffected As Integer

            //        result = (db.RunNonQueryProcedure("sp_delete_schedule", parameters, rowsAffected) = 0)

            //        db.Connection.Close()
            //        db.Connection.Dispose()
            //    Else
            //        Throw New ArgumentException("DeleteSchedule(): ScheduleID must be greater than 0")
            //    End If

            //    Return result
            //End Function
        }

        public static int PublicationEstimatedTimeBehind(Happy.MI.Replication.Subscription s)
        {

            PublicationMonitor mon = new PublicationMonitor();
            mon.DistributionDBName = s.Publication.Dist.Database;
            mon.PublisherName = s.Publication.Src.ServerName;
            mon.PublicationDBName = s.Publication.Src.Database;
            mon.Name = s.Publication.PublicationName;
            mon.ConnectionContext = s.Publication.Src.ServerConnection;
            DataSet ds1 = mon.EnumSubscriptions2(SubscriptionResultOption.AllSubscriptions);
            ds1.WriteXml(@"c:\desktop\ds1.xml");

            //ds.Tables[0].ToString();


            if (mon.LoadProperties())
            {
                PendingCommandInfo pci = mon.TransPendingCommandInfo(s.Dest.ServerName, s.Dest.Database, SubscriptionOption.Push);
                return pci.EstimatedTimeBehind;
            }
            else
            {
                throw new Exception(string.Format("Unable to load properties for subscription [{0}][{1}]",s.Dest.ServerName, s.Publication.PublicationName));
            }
        }

        public static int TraceTokenPost(Happy.MI.Replication.Subscription s)
        {
            TransPublication TransPub = new TransPublication();
            TransPub.ConnectionContext = s.Publication.Src.ServerConnection;
            TransPub.Name = s.Publication.PublicationName;
            TransPub.DatabaseName = s.Publication.Src.Database;
            if (TransPub.LoadProperties())
            {
                return TransPub.PostTracerToken();
            }
            return 0;
        }
        public static bool TraceTokenReceive(Happy.MI.Replication.Subscription s, int TokenId){
            PublicationMonitor mon = new PublicationMonitor();
            mon.DistributionDBName = s.Publication.Dist.Database;
            mon.PublisherName = s.Publication.Src.ServerName;
            mon.PublicationDBName = s.Publication.Src.Database;
            mon.Name = s.Publication.PublicationName;
            mon.ConnectionContext = s.Publication.Src.ServerConnection;
            if (mon.LoadProperties())
            {
                DataSet ds= mon.EnumTracerTokenHistory(TokenId);
                int latency;
                string str = ds.Tables[0].Rows[0]["overall_latency"].ToString();
                bool res = int.TryParse(str, out latency);

                return res;
            }
            else
            {
                throw new Exception(string.Format("Unable to load properties for subscription [{0}][{1}]", s.Dest.ServerName, s.Publication.PublicationName));
            }
        }

        public static void Cmd(string cnct)
        {
            string script = System.IO.File.ReadAllText(@"C:\tfs\CA\Dev\MI\Happy.MI\PostReplicationScripts\GC1.txt");
            SqlConnection connection = new SqlConnection(cnct+";Connection Timeout=5");
            Server server = new Server(new ServerConnection(connection));
            //server.ConnectionContext.InfoMessage += new System.Data.SqlClient.SqlInfoMessageEventHandler(ConnectionContext_InfoMessage);
            server.ConnectionContext.ExecuteNonQuery(script);
            server.ConnectionContext.Disconnect();
        }
    }
于 2013-10-09T22:32:04.520 回答
2

看看 ScaleArc。它是 SQL 连接管理器,它通过跨多个实例对读取和写入进行分区来实现负载平衡。这确实意味着您必须签署做复制..

于 2013-10-09T22:28:53.093 回答
2

您可以查看分区表,以报告/BI 操作不会影响您的日常 OLTP 性能的方式对数据进行分区。当您需要清除旧数据时,它还可以节省一些宝贵的时间。

于 2013-10-08T13:49:44.080 回答
0
于 2013-10-14T18:17:15.507 回答
0

您可以创建快照复制,将一个数据库用于生产,另一个用于报告。将用于报告的索引移动到报告数据库中,并将应用程序所需的其他索引保留在应用程序使用的数据库中。

于 2013-10-15T03:20:59.063 回答