16 Ocak 2017 Pazartesi

Transparent Data Encryption ile tempdb ilişkisi

Transparent Data Encryption (TDE) özelliğini sanırım tüm SQL Server veritabanı yöneticileri duymuştur. SQL Server 2008 ile gelen bir özellik. Veritabanı dosyalarını ve yedek dosyalarını şifrelemek için kullanılan bir güvenlik yöntemi.

Geçenlerde ingilizce SQL Server Database Engine Microsoft forumunda bir kullanıcı test ortamındaki bazı veritabanları için TDE'yi etkinleştirdiğini fakat testlerden sonra TDE özelliğini ilgili veritabanlarından kaldırdığı halde tempdb'nin hala şifreli olduğunu ve bunu nasıl kaldıracağını sordu. Ben de test ortamımda bunun testini yaptım, kullanıcıya cevabını verdim ve bu konuda kısa bir blog yazısıyla sizlerle de paylaşmak istedim. Bu yazımda TDE'nin nasıl etkinleştirileceğine veya kullanılacağına değil, tempdb ile olan ilişkisine değineceğim.

"TDE veritabanı bazında etkinleştirilen bir özellik, tempdb ile ne alakası var?" diye soracak olursanız "sonuç itibariyle tempdb tüm veritabanları tarafından ve SQL Server tarafından kullanılan ortak bir alan, eğer bir veritabanını TDE ile şifreliyorsanız o zaman demek ki kayıtlar hassastır ve yanlış ellere geçmesini istemiyorsunuzdur, bu nedenle de verinin işlenme sürecinin bir kısmının şifresiz yapılmasını da istemezsiniz" derim. Bu iş bir bütün nihayetinde, bu nedenle TDE ile şifrelediğiniz bir veritabanındaki bir kod ile tempdb veritabanında temp bir tablodaki kayıtların da şifreli olması gerekir ve sonuç itibariyle SQL Server Instance'ında tek bir tane tempdb vardır. Bu nedenle bir SQL Server Instance'ında eğer bir tane veritabanını bile TDE ile şifrelerseniz, tempdb de otomatik olarak şifrelenir.

Peki SQL Server Instance'ınızda TDE ile şifrelenmiş veritabanlarını nasıl görebilirsiniz?

SELECT [is_encrypted], [name] FROM sys.databases WHERE [is_encrypted] = 1;
GO

Eğer SQL Server 2014 SP2 (ve muhtemelen bazı başka versiyonlar da) kullanıyorsanız "Ama sen daha az önce 'bir veritabanı TDE ile şifrelendiyse tempdb de şifrelenir' demiştin? Bu listede tempdb'yi göremiyorum?" diyebilirsiniz diye tedbirliyim, çünkü benim test ortamımda SQL Server 2014 SP2 var ve bende tempdb veritabanı yukarıdaki sorgu ile [is_encrypted] = 1 olarak görünmüyor... Bu hata için de açılan hata kaydı burada

Peki diyelim ki önceden TDE'yi etkinleştirdiğiniz, ama sonra herhangi bir sebeple bundan vazgeçip kaldırdığınız bir üretim ortamınız var. TDE'yi ilgili tüm canlı veritabanlarından kaldırdınız, peki ya tempdb? tempdb'nin şifrelemesi de kalktı mı dersiniz? Eğer SQL Server Database Engine servisinizi henüz kapatıp açmadıysanız kalkmadı efendim! "Peki yukarıdaki sorgu da işe yaramıyor, nereden anlayacağım bunu?" diye soruyorsunuz haliyle "aşağıdaki sorguyu kullanabilirsiniz" diyorum.

SELECT DB_NAME(database_id) AS [database_name], [encryption_state] FROM sys.dm_database_encryption_keys WHERE [encryption_state] = 3;

Eğer yukarıdaki sorgu sonucunda tempdb yoksa, o zaman demektir ki bu SQL Server Instance'ında TDE ile şifrelenmiş hiçbir veritabanı yoktur ve tempdb'niz de şifreli değildir. Eğer [encryption_state] = 1 olan veritabanınız varsa ise o zaman veritabanında Database Encryption Key var, ama veritabanı TDE ile aktif olarak şifrelenmemiş demektir. Eğer olur da daha farklı rakamlar görürseniz o zaman daha fazla bilgi için sizi dokümantasyona davet ediyorum.

Bunca curcunadan sonra gelgelelim "TDE'yi veritabanları için kapattıysak neden tempdb için bu kadar kaygılanalım ki?" sorusunun cevabına. Efendim baştan da dediğim gibi, tempdb ortak bir alan ve siz başka veritabanları için TDE'yi etkinleştirmemiş olsanız bile, hatta tüm veritabanlarından TDE'yi kaldırmış olsanız bile tempdb hala şifrelenmeye devam edecek, şayet SQL Server Database Engine servisi yeniden başlatılana kadar. Yani artık o SQL Server Instance'ında kaç tane veritabanınız varsa, tempdb'de ne kadar işlem yapılıyorsa, tüm o işlemler şifrelenerek yapılacak. Bunun tabii ki bir performans bedeli var. Durduk yere, TDE kullanmıyorken bu bedeli neden ödeyesiniz? Bu yazı da işte bu konuda sizleri uyarmak içindi.

Bu yazıyı yazdıktan sonra Microsoft Azure'da test amacıyla bulunan ve üstünde SQL Server 2016 SP1 olan sanal makinemde de aynı testi yaptım. Aynı test, SQL Server 2014 SP2 ve SQL Server 2016 SP1'de ayrı ayrı hataları (Bug) ortaya çıkardı. Microsoft'tan yapılan yorumlara göre bu hatalar "metadata" düzeyindeki hatalar. Yani [is_encrypted]'ın sonucu 0 olacağına 1 oluyor, ama aslında arkaplanda her şey düzgün çalışıyor şeklinde oldu. Bu konuda Microsoft'a açtığım hata kaydının ayrıntıların buradan ulaşabilirsiniz.

Sevgiler,
Ekrem Önsoy

10 Ocak 2017 Salı

Yedek alırken bütünlük kontrolü yapıyor musunuz?

Aralık ayı içerisinde 2 farklı yazı ile (biri ve diğeri) yedek almanın önemine değinmiştim. Bu yazı ile de, SQL Server'da yedek alırken kullanılabilecek önemli bir parametreye değinmek istiyorum.

Biliyorum, birçok kurumun maalesef hala belirlenmiş RPO ve RTO değerleri yok ve yedek alma işi sadece "yapılması gereken standart işlerden bir diğeri" gibi görülüyor. Bir felaket durumunda da tabiri caizse elde harddisk "verilerimizi nasıl kurtarabiliriz?" diye kapı kapı dolanan arkadaşları görüyoruz. Bu yazım ise tabii ki RPO ve RTO değerlerinin anlamını bilip, yedek almaya gerekli özeni gösteren kurum ve çalışanları için.

Konuya giriş yapmam için önece bazı terim ve kavramları izah etmem gerekiyor.

SQL Server'da veriler en küçük depolama birimi olan Page'lere kaydedilir. Page'ler diskte 8 kilobaytlık bir alan kaplar. Bir Page tablo yapınızda kullandığınız veritipleriniz ve verinize göre bir veya daha fazla kayıt içerebilir. SQL Server verileri Page'lere kaydettikten sonra otomatik ve rutin bir şekilde tekrar tekrar dönüp Page'lerin sağlık durumunu kontrol etmez ve Page'lerin sağlık ve bütünlük durumu fiziksel disk yapısındaki fiziksel bir sıkıntıdan, SQL Server'daki bir Bug'tan veya SQL Server'ın sağlıklı bir şekilde kapatılmaması gibi sebeplereden dolayı bozulabilir.

SQL Server 2005 ile birlikte Page'lerin veri bütünlüğünü kontrol seçenekleri arasına Checksum da katıldı. Öncesinde ise 2 seçenek vardı, ya bu kontrol hiç yapılmasın ya da Torn Page, bu seçenekler yeterince koruma sağlamıyordu, bu nedenle Checksum seçeneği geldi ve SQL Server 2005 ile birlikte varsayılan seçenek oldu. 

Page bütünlüğü için Checksum seçeneği kullanıldığında SQL Server tüm Page'in bütünlüğünü dikkate alarak bir Checksum hesaplar ve bunu da Page diske yazılırken Page'in içerisindeki Page Header'a "m_tornBits" etiketiyle kaydeder. Bu Page diskten okunurken Checksum tekrar hesaplanır ve "m_tornBits"teki değer ile (örnek değer: -1433260509) karşılaştırılır. Eğer eşleşme sağlanamazsa, o zaman SQL Server Error Log ve Windows Application Event Log'a 824 hata numarasıyla kaydedilir. Bu sayede üst seviye bütünlük kontrolü sağlanmış olur.

Torn Page'in yeterince koruma sağlamadığını söylemiştim, Checksum ile farkını belirginleştirmek için bir örnek vereyim. Sanırım birçoğunuz hayatınızın bir döneminde bir Hex Editor kullanmıştır. Örneğin Page doğrulama özelliği Torn Page olan bir sayfayı, ilgili veritabanını Offline duruma alarak bir Hex Editor ile değiştirebilirsiniz ve DBCC CHECKDB ile bütünlük kontrolü yapmadığınız sürece bu değişikliği kimse fark etmez. Fakat aynı Page Checksum doğrulamasıyla ayarlansaydı, Hex Editor ile değişiklik yapıp veritabanını tekrar Online duruma getirip ilgili kayıtları sorguladığınızda aşağıdaki gibi bir hata alırdınız:

Msg 824, Level 24, State 2, Line 1
SQL Server detected a logical consistency-based I/O error: incorrect checksum (expected: 0x34bf84e5; actual: 0x31268142). It occurred during a read of page (1:654) in database...

"Peki tüm bunların yedekleme ile ne ilgisi var kardeşim?" deme noktasına getirdiysem sizi, elbet bir nedeni var. Açıklayayım efendim.

SQL Server 2014 ile birlikte yedek alırken Checksum kontrolü seçeneği de geldi. Bu özellik 2 şekilde kullanılabilir.

1- Instance düzeyinde, "sp_configure" ile aşağıdaki gibi etkinleştirilebilir.

EXEC sp_configure 'backup checksum default', 1;
RECONFIGURE
GO

2- Her bir yedek alma komutuyla birlikte aşağıdaki gibi kullanılabilir:

BACKUP DATABASE X TO DISK = N'xxx' WITH CHECKSUM

Eğer birinci seçeneği kullanırsanız, veritabanı yedekleriniz varsayılan olarak Checksum kontrolü yapılarak alınır ve ilgili sistem tablolarında da kayıtları aşağıdaki gibi görürsünüz:


Bu kullanım size pratikte yukarıda bahsini ettiğim örnekte olduğu gibi bütünlük uyuşmazlığı yaşanan durumlarda haberdar olmanızı sağlayacaktır.

Örneğin test ortamımda bir sayfasını kasten bozduğum bir veritabanım var. Bu veritabanımın yedeğini aşağıdaki komut ile aldığımda hiçbir hata ile karşılaşmıyorum:

USE [master]
GO
BACKUP DATABASE [AdventureWorks2012_corruptionTest] TO DISK = N'C:\temp\corrupt_db.bak';
GO

Fakat aynı yedeği aşağıdaki komut ile alırsam (veya yukarıda 1. seçenek olarak belirttiğim gibi bu özelli Instance düzeyinde etkinleştirirsem)

USE [master]
GO
BACKUP DATABASE [AdventureWorks2012_corruptionTest] TO DISK = N'C:\temp\corrupt_db.bak' WITH CHECKSUM;
GO

O zaman aşağıdaki gibi bir hata alıyorum:

Msg 3043, Level 16, State 1, Line 6
BACKUP 'AdventureWorks2012_corruptionTest' detected an error on page (1:11984) in file 'C:\temp\AdventureWorks2012_Data.mdf'.
Msg 3013, Level 16, State 1, Line 6
BACKUP DATABASE is terminating abnormally.

Sonuç itibariyle, bu tür sorunlardan en kısa sürede haberdar olmak için:
- Tüm veritabanlarınızın Page Verification özelliğinin CHECKSUM olduğundan,
- SQL Server 2014 ve üzeri versiyonlardaki Instance'larınızda "backup checksum default" özelliğini etkinleştirdiğinizden,
- Tüm kritik veritabanlarınız için düzenli olarak DBCC CHECKDB kontrolü yaptığınızdan emin olun.

Olası bir bütünlük sorunundan ne kadar erken haberdar olursanız veri kaybı yaşama olasılığınız o kadar düşük olur. Bu nedenle uygulamalarınızın ihtiyaçlarını da düşünerek olabildiğince çok güvenlik önlemi almanız gerekiyor. Böyle derken kastettiğim şu Checksum ve DBCC CHECKDB gibi uygulamaların elbette bir kaynak bedeli var, CPU ve IO yükünü arttırırlar. Hangi işlemi ne zaman ve nasıl yaptığınız çok önemli. Operasyonlarınızı da aksatmak istemezsiniz, güvenlikten de feragat etmek istemezsiniz. Yarın üzülmemek için kendi ortamınızda, kendi uygulamalarınızın ihtiyaçlarını ve donanım kaynaklarınızı da dikkate alarak testlerinizi yapmalı ve bu uygulamaları devreye almalısınız.

Ekrem Önsoy

3 Ocak 2017 Salı

Yanlış veritipi kullanımının maliyeti

Buruk girdiğimiz 2017'nin ilk yazısı...

Bir müşterimde tablolardaki Clustered Indeksler için Int veritipi yerine Uniqueidentifier veritipinin kullanıldığını gördüm. Önce, en iyi pratiklere göre hangi alanın Clustered Indeks olarak tanımlanabileceğini tekrar hatırlayalım:

- Kısa olmalı,
- Eşsiz değerleri barındırmalı
- Sıralı artan değerleri barındırmalı,
- Değeri değişmemeli,

Konuştuğumuz arkadaşım bana bu tasarım kararını edindikleri kaynaklara binaen aldıklarını söyledi. Bu kaynakların neler olduğunu henüz net olarak bilmiyorum; fakat bu fikir benim bildiğim, uyguladığım ve iyi çalıştığını gördüğüm iyi pratiklerin tam tersiydi. Bu nedenle bu fikre katılamadım, bununla birlikte test etmek istedim. Bu test sonuçlarını sizlerle de paylaşıyorum.

Hatırlatmak gerekirse, Int veritipindeki bir alanın diskte kapladığı alan maliyeti 4 bayttır, Uniqueidentifier alanınki ise 16 bayttır, yani 4 kat daha fazla!

Test için AdventureWorks2014 veritabanındaki Person.Person tablosunu kullandım. Bu tabloyu 2 kere kopyaladım. Birinde, aslındaki gibi BusinessIdentifyId alanını Clustered Indeks olarak tanımladım, diğerine ise Rowguid alanını Clustered Indeks olarak tanımladım. Tablolarda bu iki indeksten başka herhangi bir indeks yoktu.

Daha sonra 2 tablodan da aşağıdaki gibi 1000'er adet kayıt sorguladım:

SELECT TOP (1000) * FROM [dbo].[Person];

BusinessIdentityId alanının Clustered Indeks olduğu tablodan 1000 kaydın okunması için 80 Read yapıldı. Rowguid alanının Clustered Indeks olduğu tablodan 1000 kaydın okunması için ise 188 Read yapıldı. 2,35 kat daha fazla!

Daha sonra iki tabloda da hem Lastname ve Firstname'den oluşan, hem de ModifiedDate için ikişer tane Nonclustered Indeks oluşturdum. Bunun akabinde BusinessIdentityId alanının Clustered Indeks olduğu tablodaki toplam indeks boyutu 1,240kb, Rowguid alanının Clustered Indeks olduğu tablodaki toplam indeks boyutu ise 1,760kb oldu, bu da %42 kat daha fazla demek. Bunun nedeni ise, Clustered olan her tablodaki Nonclustered indekslerin Cluster Key'leri içermesidir.

Clustered Indeks olarak belirlenen alanların genelde Primary Key olduğunu ve ilişkili oldukları ikincil tablolarda da aynı alanların olduğunu hatırlarsak, maliyet iki katına çıkıyor. Join işlemlerinin de bu alanlar üstünden yapıldığını hatırlamakta fayda var, bu da ciddi bir maliyet.

Kayıtların Page'lerde daha fazla yer kaplaması, Page'lerin daha az sayıda kayıt tutabileceği anlamına gelir. Bu da daha fazla kayıt okumak için, daha fazla Page'in hem diskte hem de hafızada yer tutması gerektiği anlamına gelir. Bu da kaynak kullanımını verimsiz hale getirir, performans düşüklüğüne neden olur.

Bu örneğe göre eğer verinizi Int bir alanda saklamak varken (tabii ki iş gereksinimleri buna izin verdiği müddetçe) Uniqueidentifier veritipini seçerseniz, disk ve hafıza maliyetlerinizi ortalama 4 kat arttıracaksınız demektir. Tasarım yaparken bunu bilmekte ve maliyet hesabını buna göre yapmakta büyük fayda var.

Sevgiler,
Ekrem Önsoy

26 Aralık 2016 Pazartesi

Azure Standard Storage'larınızda TRIM etkin mi?

Microsoft Azure sanal makinelerinizde kullandığınız Standard Storage (HDD)'lerdeki TRIM özelliğinden haberdar mısınız? Eğer değilseniz, Microsoft Azure'da sanal diskleriniz varsa ve Storage maliyetlerinizi düşürmek istiyorsanız okumaya devam edin.

Efendim artık öyle veya böyle bulut projelerine bulaşmak kaçınılmaz oldu. Bu konuda da edindiğim tecrübe ve bilgileri olabildiğince sizlerle paylaşıyor olacağım.

Bu aralar benim de dahil olduğum bir Azure projesinde çalışırken oluşturulan bazı disklerde TRIM kullanılmadığını gördüm. Eğer TRIM kullanılmazsa ne mi olur? Diskiniz için ne kadar alan tanımlandıysa, ki bu alan miktarı an itibariyle azami 1TB'tır, o kadar alan boyutu için ücretlendirilirsiniz. Fakat o disk için TRIM özelliği etkinleştirilmişse, o zaman sadece o diskte kullandığınız alanlar kadar ücretlendirilirsiniz.

Misal yeni veya varolan bir sanal makineniz için 1TB boyutunda bir Standard Storage disk oluşturdunuz ve kullanmaya başladınız. Bu diskteki dosya veya dosyalar sabit belli bir boyutta değil, zaman zaman büyüyor, zaman zaman küçülüyor. İşte özellikle böyle senaryolar için TRIM özelliğini kullanırsanız Azure faturanız azalır.

Peki TRIM özelliğinin etkin olup olmadığını nasıl anlayabilirsiniz? Diskin bağlı olduğu sanal makineye bağlanın ve Command Prompt'tan aşağıdaki komutu çalıştırın:

fsutil behavior query DisableDeleteNotify

Eğer yukarıdaki komut sonuç olarak 0 döndürüyorsa o zaman TRIM etkin demektir, eğer sonuç olarak 1 dönüyorsa o zaman TRIM etkin değildir. TRIM'i etkinleştirmek için aşağıdaki komutu çalıştırabilirsiniz.

fsutil behavior set DisableDeleteNotify 0

Kaynak:
https://docs.microsoft.com/en-us/azure/virtual-machines/virtual-machines-windows-attach-disk-portal?toc=%2fazure%2fvirtual-machines%2fwindows%2ftoc.json


Ekrem Önsoy

19 Aralık 2016 Pazartesi

Varolan donanım kaynaklarınızı gerçekten verimli kullanıyor musunuz?

Microsoft SQL Server ürününün, eski önyargılar nedeniyle birçok yönetici gözünde doğru yerde olmadığını düşünüyorum. Evet SQL Server eskiden, özellikle 2000 ve öncesi versiyonlarda gerçekten çok ilkeldi. Birçok sıkıntıları vardı. Özellikle SQL Server 2005 ile başlayan ve gün itibariyle gelinen noktada SQL Server 2016 ve Azure ile çok daha farklı bir noktada. Bunu 10.000+ kullanıcılı bir ortamda SQL Server 2000 kullanan biri olarak rahatlıkla söyleyebilirim.

Böyle bir giriş yapmamın nedeni, geçenlerde büyük bir firmanın yöneticisinin masaya oturduğu anda bana şöyle demesi:
- "Biliyorum, SQL Server zaten performanslı çalışmayan bir ürün, haliyle Oracle seviyesinde performans alabilmemiz için daha fazla donanıma ihtiyacı var."
- "Bu kadar geçmişte kaldığınız için üzgünüm bayım, ama bu çok demode bir fikir!" diyemedim ne yazık ki, ortam müsait değildi.

İpucu: İlgili firmada sadece birkaç günlük bir çalışma ile %66 performans iyileştirmesi sağladık!

Eğer bugün hala SQL Server veritabanlarınızdaki performanstan şikayetçiyseniz, ilk bakacağınız şeyler veritabanı sunucunuzun yapılandırılması / kurgusu, en iyi pratiklerin uygulanıp uygulanmadığı ve uygulamalarınızın çalıştırdığı veritabanı kodlarının "performans" özelliği ile birlikte yazılıp yazılmaması olmalı. Yazılımlar KOBİ'dekilerden büyük firmadakilere kadar daha ziyade "işlev" odaklı kod yazılıyor. Genellikle "Ekran benim makinemde / test ortamında iş tarafından talep edildiği gibi çalışıyor, kafi!" düşüncesi hakim. Maalesef hala birçok kurumda yazılım yazılırken "performans" ve "güvenlik" konuları birer özellik olarak görülmüyor. Neyse, bunlar belki başka bir yazının veya başka bir yazarın konusu. Bu yazımda başka bir konuya değineceğim.

Efendim SQL Server 2012 Standard Edition olan kritik ortamlardan birinde SQL Server sunucusunun CPU yapılandırmasında bir ilginçlik gördüm. Durum aşağıdaki gibiydi:

6 tane CPU soketi görünüyor, 2 tanesi kullanım dışı
Daha sonra kullanım dışı (OFFLINE) görünen 4 ve 5 numaralı CPU soketlerini başka bir yolla daha kontrol ettim:


Bu makinede 6 tane CPU soket, her sokette de 2'şer tane Core var görünüyordu. Yani toplamda 12 Core vardı.

Bu ortamdaki Log'larda şöyle bir mesaj görüyordum:
"SQL Server detected 6 sockets with 2 cores per socket and 2 logical processors per socket, 12 total logical processors; using 8 logical processors based on SQL Server licensing. This is an informational message; no user action is required."


Yani makinede 12 Core olmasına rağmen, SQL Server bunun 8'ini görebiliyor, CPU kaynaklarının %33'ünden faydalanılamıyordu. Neden mi? Çünkü SQL Server 2012 Standard Edition'ın 4 soket lisanslama sınırlaması var. SQL Server 2012 Standard Edition'da 4'ten fazla CPU Soketi, 16'dan fazla Core kullanılamaz. Sonuç olarak israf edilen %33'lük bir CPU donanımı söz konusu, ama ilgili yöneticiler SQL Server'ın tüm varolan kaynağı kullandığını düşünüyor. Emin olabilirsiniz, daha farklı ortamlarda bundan çok çok daha büyük israf oranlarını gördüm.

İlave örnek: İsraf oranlarının nereye varabileceği hakkında sadece fikir vermesi açısından aşağıdaki mesaja bakın bir de, ama o hikayenin ayrıntılarına burada girmeyeceğim. Bu bir kurgu değildir, gerçek bir ortam.

SQL Server detected 8 sockets with 8 cores per socket and 8 logical processors per socket, 64 total logical processors; using 20 logical processors based on SQL Server licensing. This is an informational message; no user action is required.


Nasıl ama? CPU kaynaklarının %68,75'i SQL Server tarafından kullanılamıyor.


Asıl hikayemize dönersek, söz konusu makine bir sanal makineydi, ben de ilgili sistem yöneticilerine bu sanal makinenin CPU ayarlarının SQL Server lisanslaması açısından doğru olmadığını, değiştirmemiz gerektiğini ilettim. Ufak bir planlı kesintiyle, artık SQL Server makinedeki 12 Core'dan yani CPU kaynaklarının tümünden faydalanabiliyordu. SQL Server için herhangi bir CPU modelini kullanmamalısınız. Bir SQL Server sunucu için CPU seçimi özenle yapılmalıdır. Makinenin sanal veya fiziksel oluşu da dikkate alınmalıdır. Bu sadece performans açısından değil, lisanslama maliyeti açısından da çok önemlidir. Sırf yapacağınız yanlış bir CPU modeli seçimiyle bile işe onbinlerce dolar zararla başlayabilirsiniz.

Bu yapılandırma işinin tabii ki daha hafıza, disk ve ağ altyapısı ayakları var. Sonra da veritabanı tasarımı ve kodlar...

Demem o ki söylenmek, kızmak kolay; ama gerçekten de ürün mü sıkıntılı, yoksa hakkını veremeyenler mi? Birçok yönetici giderleri kısmak için yollar arıyor, ama "varolan kaynaklardan gerçekten verimli yararlanılabiliyor mu?" sorusu sorulmuyor.

Eğer sizin de farkında olduğunuz veya kuşkulandığınız benzer sorunlarınız varsa iletişime geçmek için beklemeyin.

İyi işler!
Ekrem Önsoy